From 59584492005b9daa2e44643f099a747a9d6ad4a8 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sat, 20 Dec 2025 08:56:45 +0200 Subject: [PATCH 01/23] PoC: split away server express and hono deps into server-express and server-hono --- common/eslint-config/eslint.config.mjs | 1 + docs/server.md | 4 +- examples/server/README.md | 5 +- examples/server/package.json | 2 + examples/server/src/elicitationFormExample.ts | 5 +- examples/server/src/elicitationUrlExample.ts | 4 +- .../src/honoWebStandardStreamableHttp.ts | 3 +- .../server/src/jsonResponseStreamableHttp.ts | 3 +- examples/server/src/simpleSseServer.ts | 3 +- .../src/simpleStatelessStreamableHttp.ts | 3 +- examples/server/src/simpleStreamableHttp.ts | 4 +- examples/server/src/simpleTaskInteractive.ts | 2 +- .../sseAndStreamableHttpCompatibleServer.ts | 9 +- examples/server/src/ssePollingExample.ts | 3 +- .../src/standaloneSseWithGetStreamableHttp.ts | 3 +- examples/server/tsconfig.json | 2 + examples/shared/package.json | 1 + .../shared/src/demoInMemoryOAuthProvider.ts | 36 +- .../test/demoInMemoryOAuthProvider.test.ts | 234 +------- examples/shared/tsconfig.json | 1 + package.json | 2 +- packages/client/src/client/sse.ts | 3 +- packages/server-express/README.md | 81 +++ packages/server-express/eslint.config.mjs | 12 + packages/server-express/package.json | 67 +++ .../server-express/src/auth/bearerAuth.ts | 62 ++ packages/server-express/src/auth/router.ts | 172 ++++++ .../server => server-express/src}/express.ts | 0 packages/server-express/src/index.ts | 4 + .../src/middleware/hostHeaderValidation.ts | 52 ++ .../test/server/auth/router.test.ts | 28 +- packages/server-express/tsconfig.json | 14 + packages/server-express/tsdown.config.ts | 23 + packages/server-express/vitest.config.js | 3 + packages/server-hono/README.md | 64 ++ packages/server-hono/eslint.config.mjs | 12 + packages/server-hono/package.json | 63 ++ packages/server-hono/src/auth/router.ts | 33 ++ packages/server-hono/src/index.ts | 3 + .../src/middleware/hostHeaderValidation.ts | 33 ++ packages/server-hono/src/streamableHttp.ts | 14 + packages/server-hono/test/server-hono.test.ts | 114 ++++ packages/server-hono/tsconfig.json | 14 + packages/server-hono/tsdown.config.ts | 23 + packages/server-hono/vitest.config.js | 3 + packages/server/package.json | 5 - packages/server/src/index.ts | 2 +- .../src/server/auth/handlers/authorize.ts | 112 ++-- .../src/server/auth/handlers/metadata.ts | 42 +- .../src/server/auth/handlers/register.ts | 106 ++-- .../server/src/server/auth/handlers/revoke.ts | 109 ++-- .../server/src/server/auth/handlers/token.ts | 120 ++-- packages/server/src/server/auth/index.ts | 1 + .../server/auth/middleware/allowedMethods.ts | 26 +- .../src/server/auth/middleware/bearerAuth.ts | 132 +++-- .../src/server/auth/middleware/clientAuth.ts | 75 +-- packages/server/src/server/auth/provider.ts | 3 +- .../server/auth/providers/proxyProvider.ts | 5 +- packages/server/src/server/auth/router.ts | 126 ++-- packages/server/src/server/auth/web.ts | 140 +++++ .../server/middleware/hostHeaderValidation.ts | 122 ++-- packages/server/src/server/sse.ts | 12 +- packages/server/src/server/streamableHttp.ts | 142 ++++- .../server/auth/handlers/authorize.test.ts | 300 ++-------- .../server/auth/handlers/metadata.test.ts | 65 ++- .../server/auth/handlers/register.test.ts | 297 +--------- .../test/server/auth/handlers/revoke.test.ts | 279 ++------- .../test/server/auth/handlers/token.test.ts | 545 +++-------------- .../auth/middleware/allowedMethods.test.ts | 88 +-- .../server/auth/middleware/bearerAuth.test.ts | 548 +++--------------- .../server/auth/middleware/clientAuth.test.ts | 133 ++--- .../auth/providers/proxyProvider.test.ts | 27 +- pnpm-lock.yaml | 163 ++++-- test/integration/package.json | 1 + .../test_1277_zod_v4_description.test.ts | 3 +- test/integration/test/server.test.ts | 24 +- test/integration/test/server/mcp.test.ts | 12 +- .../stateManagementStreamableHttp.test.ts | 7 +- test/integration/test/taskLifecycle.test.ts | 3 +- .../integration/test/taskResumability.test.ts | 3 +- test/integration/test/title.test.ts | 3 +- test/integration/tsconfig.json | 1 + 82 files changed, 2339 insertions(+), 2670 deletions(-) create mode 100644 packages/server-express/README.md create mode 100644 packages/server-express/eslint.config.mjs create mode 100644 packages/server-express/package.json create mode 100644 packages/server-express/src/auth/bearerAuth.ts create mode 100644 packages/server-express/src/auth/router.ts rename packages/{server/src/server => server-express/src}/express.ts (100%) create mode 100644 packages/server-express/src/index.ts create mode 100644 packages/server-express/src/middleware/hostHeaderValidation.ts rename packages/{server => server-express}/test/server/auth/router.test.ts (95%) create mode 100644 packages/server-express/tsconfig.json create mode 100644 packages/server-express/tsdown.config.ts create mode 100644 packages/server-express/vitest.config.js create mode 100644 packages/server-hono/README.md create mode 100644 packages/server-hono/eslint.config.mjs create mode 100644 packages/server-hono/package.json create mode 100644 packages/server-hono/src/auth/router.ts create mode 100644 packages/server-hono/src/index.ts create mode 100644 packages/server-hono/src/middleware/hostHeaderValidation.ts create mode 100644 packages/server-hono/src/streamableHttp.ts create mode 100644 packages/server-hono/test/server-hono.test.ts create mode 100644 packages/server-hono/tsconfig.json create mode 100644 packages/server-hono/tsdown.config.ts create mode 100644 packages/server-hono/vitest.config.js create mode 100644 packages/server/src/server/auth/web.ts diff --git a/common/eslint-config/eslint.config.mjs b/common/eslint-config/eslint.config.mjs index 321f3f6fc..6ac057c69 100644 --- a/common/eslint-config/eslint.config.mjs +++ b/common/eslint-config/eslint.config.mjs @@ -47,6 +47,7 @@ export default defineConfig( '@typescript-eslint/consistent-type-imports': ['error', { disallowTypeAnnotations: false }], 'simple-import-sort/imports': 'warn', 'simple-import-sort/exports': 'warn', + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'import/no-extraneous-dependencies': [ 'error', { diff --git a/docs/server.md b/docs/server.md index 4d5138e84..800d336db 100644 --- a/docs/server.md +++ b/docs/server.md @@ -70,7 +70,7 @@ For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` to create an Express app with DNS rebinding protection enabled by default: ```typescript -import { createMcpExpressApp } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; // Protection auto-enabled (default host is 127.0.0.1) const app = createMcpExpressApp(); @@ -85,7 +85,7 @@ const app = createMcpExpressApp({ host: '0.0.0.0' }); When binding to `0.0.0.0` / `::`, provide an allow-list of hosts: ```typescript -import { createMcpExpressApp } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; const app = createMcpExpressApp({ host: '0.0.0.0', diff --git a/examples/server/README.md b/examples/server/README.md index 310113e45..bb1216a04 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -1,6 +1,9 @@ # MCP TypeScript SDK Examples (Server) -This directory contains runnable MCP **server** examples built with `@modelcontextprotocol/server`. +This directory contains runnable MCP **server** examples built with `@modelcontextprotocol/server` plus framework adapters: + +- `@modelcontextprotocol/server-express` +- `@modelcontextprotocol/server-hono` For client examples, see [`../client/README.md`](../client/README.md). For guided docs, see [`../../docs/server.md`](../../docs/server.md). diff --git a/examples/server/package.json b/examples/server/package.json index a3a3d14c7..cb37d9f40 100644 --- a/examples/server/package.json +++ b/examples/server/package.json @@ -38,6 +38,8 @@ "hono": "catalog:runtimeServerOnly", "@modelcontextprotocol/examples-shared": "workspace:^", "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/server-express": "workspace:^", + "@modelcontextprotocol/server-hono": "workspace:^", "cors": "catalog:runtimeServerOnly", "express": "catalog:runtimeServerOnly", "zod": "catalog:runtimeShared" diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index f8863c17b..567975662 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -9,8 +9,9 @@ import { randomUUID } from 'node:crypto'; -import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { type Request, type Response } from 'express'; +import { isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; +import type { Request, Response } from 'express'; // Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults // The validator supports format validation (email, date, etc.) if ajv-formats is installed diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index 99f85d079..79ba49a17 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -13,15 +13,13 @@ import { setupAuthServer } from '@modelcontextprotocol/examples-shared'; import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server'; import { checkResourceAllowed, - createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, isInitializeRequest, - mcpAuthMetadataRouter, McpServer, - requireBearerAuth, StreamableHTTPServerTransport, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; +import { createMcpExpressApp, mcpAuthMetadataRouter, requireBearerAuth } from '@modelcontextprotocol/server-express'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; diff --git a/examples/server/src/honoWebStandardStreamableHttp.ts b/examples/server/src/honoWebStandardStreamableHttp.ts index aef1e99e2..f5c59cffe 100644 --- a/examples/server/src/honoWebStandardStreamableHttp.ts +++ b/examples/server/src/honoWebStandardStreamableHttp.ts @@ -10,6 +10,7 @@ import { serve } from '@hono/node-server'; import type { CallToolResult } from '@modelcontextprotocol/server'; import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { mcpStreamableHttpHandler } from '@modelcontextprotocol/server-hono'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import * as z from 'zod/v4'; @@ -56,7 +57,7 @@ app.use( app.get('/health', c => c.json({ status: 'ok' })); // MCP endpoint -app.all('/mcp', c => transport.handleRequest(c.req.raw)); +app.all('/mcp', mcpStreamableHttpHandler(transport)); // Start the server const PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000; diff --git a/examples/server/src/jsonResponseStreamableHttp.ts b/examples/server/src/jsonResponseStreamableHttp.ts index 2199ebfbe..44155ea9d 100644 --- a/examples/server/src/jsonResponseStreamableHttp.ts +++ b/examples/server/src/jsonResponseStreamableHttp.ts @@ -1,7 +1,8 @@ import { randomUUID } from 'node:crypto'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleSseServer.ts b/examples/server/src/simpleSseServer.ts index 90561c62f..35b48b69d 100644 --- a/examples/server/src/simpleSseServer.ts +++ b/examples/server/src/simpleSseServer.ts @@ -1,5 +1,6 @@ import type { CallToolResult } from '@modelcontextprotocol/server'; -import { createMcpExpressApp, McpServer, SSEServerTransport } from '@modelcontextprotocol/server'; +import { McpServer, SSEServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleStatelessStreamableHttp.ts b/examples/server/src/simpleStatelessStreamableHttp.ts index 3aee2c212..0f3a78e63 100644 --- a/examples/server/src/simpleStatelessStreamableHttp.ts +++ b/examples/server/src/simpleStatelessStreamableHttp.ts @@ -1,5 +1,6 @@ import type { CallToolResult, GetPromptResult, ReadResourceResult } from '@modelcontextprotocol/server'; -import { createMcpExpressApp, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index 7613e3786..f550ed7d7 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -11,17 +11,15 @@ import type { } from '@modelcontextprotocol/server'; import { checkResourceAllowed, - createMcpExpressApp, ElicitResultSchema, getOAuthProtectedResourceMetadataUrl, InMemoryTaskMessageQueue, InMemoryTaskStore, isInitializeRequest, - mcpAuthMetadataRouter, McpServer, - requireBearerAuth, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp, mcpAuthMetadataRouter, requireBearerAuth } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index 956c33f8e..4685f33f5 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -35,7 +35,6 @@ import type { } from '@modelcontextprotocol/server'; import { CallToolRequestSchema, - createMcpExpressApp, GetTaskPayloadRequestSchema, GetTaskRequestSchema, InMemoryTaskStore, @@ -45,6 +44,7 @@ import { Server, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; // ============================================================================ diff --git a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts index 335802d0a..3ea3b71db 100644 --- a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts +++ b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts @@ -1,13 +1,8 @@ import { randomUUID } from 'node:crypto'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { - createMcpExpressApp, - isInitializeRequest, - McpServer, - SSEServerTransport, - StreamableHTTPServerTransport -} from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, SSEServerTransport, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/ssePollingExample.ts b/examples/server/src/ssePollingExample.ts index 4e3d36328..e7da09ecb 100644 --- a/examples/server/src/ssePollingExample.ts +++ b/examples/server/src/ssePollingExample.ts @@ -15,7 +15,8 @@ import { randomUUID } from 'node:crypto'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { createMcpExpressApp, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import cors from 'cors'; import type { Request, Response } from 'express'; diff --git a/examples/server/src/standaloneSseWithGetStreamableHttp.ts b/examples/server/src/standaloneSseWithGetStreamableHttp.ts index cceb24299..f9fb426cd 100644 --- a/examples/server/src/standaloneSseWithGetStreamableHttp.ts +++ b/examples/server/src/standaloneSseWithGetStreamableHttp.ts @@ -1,7 +1,8 @@ import { randomUUID } from 'node:crypto'; import type { ReadResourceResult } from '@modelcontextprotocol/server'; -import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; // Create an MCP server with implementation details diff --git a/examples/server/tsconfig.json b/examples/server/tsconfig.json index 98d3a5b3f..1f72b0199 100644 --- a/examples/server/tsconfig.json +++ b/examples/server/tsconfig.json @@ -6,6 +6,8 @@ "paths": { "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/server-express": ["./node_modules/@modelcontextprotocol/server-express/src/index.ts"], + "@modelcontextprotocol/server-hono": ["./node_modules/@modelcontextprotocol/server-hono/src/index.ts"], "@modelcontextprotocol/core": [ "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" ], diff --git a/examples/shared/package.json b/examples/shared/package.json index 8287ca552..2d0f6ebe9 100644 --- a/examples/shared/package.json +++ b/examples/shared/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/server-express": "workspace:^", "express": "catalog:runtimeServerOnly" }, "devDependencies": { diff --git a/examples/shared/src/demoInMemoryOAuthProvider.ts b/examples/shared/src/demoInMemoryOAuthProvider.ts index bcf11dd0c..23b168224 100644 --- a/examples/shared/src/demoInMemoryOAuthProvider.ts +++ b/examples/shared/src/demoInMemoryOAuthProvider.ts @@ -9,8 +9,9 @@ import type { OAuthServerProvider, OAuthTokens } from '@modelcontextprotocol/server'; -import { createOAuthMetadata, InvalidRequestError, mcpAuthRouter, resourceUrlFromServerUrl } from '@modelcontextprotocol/server'; -import type { Request, Response } from 'express'; +import { createOAuthMetadata, InvalidRequestError, resourceUrlFromServerUrl } from '@modelcontextprotocol/server'; +import { mcpAuthRouter } from '@modelcontextprotocol/server-express'; +import type { Request, Response as ExpressResponse } from 'express'; import express from 'express'; export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore { @@ -47,7 +48,7 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider { constructor(private validateResource?: (resource?: URL) => boolean) {} - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise { + async authorize(client: OAuthClientInformationFull, params: AuthorizationParams): Promise { const code = randomUUID(); const searchParams = new URLSearchParams({ @@ -64,27 +65,24 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider { // Simulate a user login // Set a secure HTTP-only session cookie with authorization info - if (res.cookie) { - const authCookieData = { - userId: 'demo_user', - name: 'Demo User', - timestamp: Date.now() - }; - res.cookie('demo_session', JSON.stringify(authCookieData), { - httpOnly: true, - secure: false, // In production, this should be true - sameSite: 'lax', - maxAge: 24 * 60 * 60 * 1000, // 24 hours - for demo purposes - path: '/' // Available to all routes - }); - } + const authCookieData = { + userId: 'demo_user', + name: 'Demo User', + timestamp: Date.now() + }; + const cookieValue = encodeURIComponent(JSON.stringify(authCookieData)); + const maxAgeSeconds = 24 * 60 * 60; // 24 hours - demo only + const setCookie = `demo_session=${cookieValue}; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSeconds}; Path=/`; if (!client.redirect_uris.includes(params.redirectUri)) { throw new InvalidRequestError('Unregistered redirect_uri'); } const targetUrl = new URL(params.redirectUri); targetUrl.search = searchParams.toString(); - res.redirect(targetUrl.toString()); + const redirectResponse = Response.redirect(targetUrl.toString(), 302); + const headers = new Headers(redirectResponse.headers); + headers.append('Set-Cookie', setCookie); + return new Response(null, { status: redirectResponse.status, headers }); } async challengeForAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise { @@ -204,7 +202,7 @@ export const setupAuthServer = ({ }) ); - authApp.post('/introspect', async (req: Request, res: Response) => { + authApp.post('/introspect', async (req: Request, res: ExpressResponse) => { try { const { token } = req.body; if (!token) { diff --git a/examples/shared/test/demoInMemoryOAuthProvider.test.ts b/examples/shared/test/demoInMemoryOAuthProvider.test.ts index 4018dddbe..0c1c887aa 100644 --- a/examples/shared/test/demoInMemoryOAuthProvider.test.ts +++ b/examples/shared/test/demoInMemoryOAuthProvider.test.ts @@ -1,21 +1,15 @@ import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; import type { AuthorizationParams } from '@modelcontextprotocol/server'; import { InvalidRequestError } from '@modelcontextprotocol/server'; -import { createExpressResponseMock } from '@modelcontextprotocol/test-helpers'; -import type { Response } from 'express'; import { beforeEach, describe, expect, it } from 'vitest'; -import { DemoInMemoryAuthProvider, DemoInMemoryClientsStore } from '../src/demoInMemoryOAuthProvider.js'; +import { DemoInMemoryAuthProvider } from '../src/demoInMemoryOAuthProvider.js'; describe('DemoInMemoryAuthProvider', () => { let provider: DemoInMemoryAuthProvider; - let mockResponse: Response & { getRedirectUrl: () => string }; beforeEach(() => { provider = new DemoInMemoryAuthProvider(); - mockResponse = createExpressResponseMock({ trackRedirectUrl: true }) as Response & { - getRedirectUrl: () => string; - }; }); describe('authorize', () => { @@ -26,7 +20,7 @@ describe('DemoInMemoryAuthProvider', () => { scope: 'test-scope' }; - it('should redirect to the requested redirect_uri when valid', async () => { + it('redirects to redirect_uri when valid', async () => { const params: AuthorizationParams = { redirectUri: 'https://example.com/callback', state: 'test-state', @@ -34,18 +28,18 @@ describe('DemoInMemoryAuthProvider', () => { scopes: ['test-scope'] }; - await provider.authorize(validClient, params, mockResponse); - - expect(mockResponse.redirect).toHaveBeenCalled(); - expect(mockResponse.getRedirectUrl()).toBeDefined(); - - const url = new URL(mockResponse.getRedirectUrl()); + const res = await provider.authorize(validClient, params); + expect(res.status).toBe(302); + const location = res.headers.get('location'); + expect(location).toBeTruthy(); + const url = new URL(location!); expect(url.origin + url.pathname).toBe('https://example.com/callback'); expect(url.searchParams.get('state')).toBe('test-state'); - expect(url.searchParams.has('code')).toBe(true); + expect(url.searchParams.get('code')).toBeTruthy(); + expect(res.headers.get('set-cookie')).toContain('demo_session='); }); - it('should throw InvalidRequestError for unregistered redirect_uri', async () => { + it('throws InvalidRequestError for unregistered redirect_uri', async () => { const params: AuthorizationParams = { redirectUri: 'https://evil.com/callback', state: 'test-state', @@ -53,212 +47,8 @@ describe('DemoInMemoryAuthProvider', () => { scopes: ['test-scope'] }; - await expect(provider.authorize(validClient, params, mockResponse)).rejects.toThrow(InvalidRequestError); - - await expect(provider.authorize(validClient, params, mockResponse)).rejects.toThrow('Unregistered redirect_uri'); - - expect(mockResponse.redirect).not.toHaveBeenCalled(); - }); - - it('should generate unique authorization codes for multiple requests', async () => { - const params1: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - state: 'state-1', - codeChallenge: 'challenge-1', - scopes: ['test-scope'] - }; - - const params2: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - state: 'state-2', - codeChallenge: 'challenge-2', - scopes: ['test-scope'] - }; - - await provider.authorize(validClient, params1, mockResponse); - const firstRedirectUrl = mockResponse.getRedirectUrl(); - const firstCode = new URL(firstRedirectUrl).searchParams.get('code'); - - // Reset the mock for the second call - mockResponse = createExpressResponseMock({ trackRedirectUrl: true }) as Response & { - getRedirectUrl: () => string; - }; - await provider.authorize(validClient, params2, mockResponse); - const secondRedirectUrl = mockResponse.getRedirectUrl(); - const secondCode = new URL(secondRedirectUrl).searchParams.get('code'); - - expect(firstCode).toBeDefined(); - expect(secondCode).toBeDefined(); - expect(firstCode).not.toBe(secondCode); - }); - - it('should handle params without state', async () => { - const params: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - codeChallenge: 'test-challenge', - scopes: ['test-scope'] - }; - - await provider.authorize(validClient, params, mockResponse); - - expect(mockResponse.redirect).toHaveBeenCalled(); - expect(mockResponse.getRedirectUrl()).toBeDefined(); - - const url = new URL(mockResponse.getRedirectUrl()); - expect(url.searchParams.has('state')).toBe(false); - expect(url.searchParams.has('code')).toBe(true); - }); - }); - - describe('challengeForAuthorizationCode', () => { - const validClient: OAuthClientInformationFull = { - client_id: 'test-client', - client_secret: 'test-secret', - redirect_uris: ['https://example.com/callback'], - scope: 'test-scope' - }; - - it('should return the code challenge for a valid authorization code', async () => { - const params: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - state: 'test-state', - codeChallenge: 'test-challenge-value', - scopes: ['test-scope'] - }; - - await provider.authorize(validClient, params, mockResponse); - const code = new URL(mockResponse.getRedirectUrl()).searchParams.get('code')!; - - const challenge = await provider.challengeForAuthorizationCode(validClient, code); - expect(challenge).toBe('test-challenge-value'); - }); - - it('should throw error for invalid authorization code', async () => { - await expect(provider.challengeForAuthorizationCode(validClient, 'invalid-code')).rejects.toThrow('Invalid authorization code'); - }); - }); - - describe('exchangeAuthorizationCode', () => { - const validClient: OAuthClientInformationFull = { - client_id: 'test-client', - client_secret: 'test-secret', - redirect_uris: ['https://example.com/callback'], - scope: 'test-scope' - }; - - it('should exchange valid authorization code for tokens', async () => { - const params: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - state: 'test-state', - codeChallenge: 'test-challenge', - scopes: ['test-scope', 'other-scope'] - }; - - await provider.authorize(validClient, params, mockResponse); - const code = new URL(mockResponse.getRedirectUrl()).searchParams.get('code')!; - - const tokens = await provider.exchangeAuthorizationCode(validClient, code); - - expect(tokens).toEqual({ - access_token: expect.any(String), - token_type: 'bearer', - expires_in: 3600, - scope: 'test-scope other-scope' - }); - }); - - it('should throw error for invalid authorization code', async () => { - await expect(provider.exchangeAuthorizationCode(validClient, 'invalid-code')).rejects.toThrow('Invalid authorization code'); - }); - - it('should throw error when client_id does not match', async () => { - const params: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - state: 'test-state', - codeChallenge: 'test-challenge', - scopes: ['test-scope'] - }; - - await provider.authorize(validClient, params, mockResponse); - const code = new URL(mockResponse.getRedirectUrl()).searchParams.get('code')!; - - const differentClient: OAuthClientInformationFull = { - client_id: 'different-client', - client_secret: 'different-secret', - redirect_uris: ['https://example.com/callback'], - scope: 'test-scope' - }; - - await expect(provider.exchangeAuthorizationCode(differentClient, code)).rejects.toThrow( - 'Authorization code was not issued to this client' - ); - }); - - it('should delete authorization code after successful exchange', async () => { - const params: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - state: 'test-state', - codeChallenge: 'test-challenge', - scopes: ['test-scope'] - }; - - await provider.authorize(validClient, params, mockResponse); - const code = new URL(mockResponse.getRedirectUrl()).searchParams.get('code')!; - - // First exchange should succeed - await provider.exchangeAuthorizationCode(validClient, code); - - // Second exchange should fail - await expect(provider.exchangeAuthorizationCode(validClient, code)).rejects.toThrow('Invalid authorization code'); - }); - - it('should validate resource when validateResource is provided', async () => { - const validateResource = vi.fn().mockReturnValue(false); - const strictProvider = new DemoInMemoryAuthProvider(validateResource); - - const params: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - state: 'test-state', - codeChallenge: 'test-challenge', - scopes: ['test-scope'], - resource: new URL('https://invalid-resource.com') - }; - - await strictProvider.authorize(validClient, params, mockResponse); - const code = new URL(mockResponse.getRedirectUrl()).searchParams.get('code')!; - - await expect(strictProvider.exchangeAuthorizationCode(validClient, code)).rejects.toThrow( - 'Invalid resource: https://invalid-resource.com/' - ); - - expect(validateResource).toHaveBeenCalledWith(params.resource); - }); - }); - - describe('DemoInMemoryClientsStore', () => { - let store: DemoInMemoryClientsStore; - - beforeEach(() => { - store = new DemoInMemoryClientsStore(); - }); - - it('should register and retrieve client', async () => { - const client: OAuthClientInformationFull = { - client_id: 'test-client', - client_secret: 'test-secret', - redirect_uris: ['https://example.com/callback'], - scope: 'test-scope' - }; - - await store.registerClient(client); - const retrieved = await store.getClient('test-client'); - - expect(retrieved).toEqual(client); - }); - - it('should return undefined for non-existent client', async () => { - const retrieved = await store.getClient('non-existent'); - expect(retrieved).toBeUndefined(); + await expect(provider.authorize(validClient, params)).rejects.toThrow(InvalidRequestError); + await expect(provider.authorize(validClient, params)).rejects.toThrow('Unregistered redirect_uri'); }); }); }); diff --git a/examples/shared/tsconfig.json b/examples/shared/tsconfig.json index aa994f939..91e368e7a 100644 --- a/examples/shared/tsconfig.json +++ b/examples/shared/tsconfig.json @@ -6,6 +6,7 @@ "paths": { "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/server-express": ["./node_modules/@modelcontextprotocol/server-express/src/index.ts"], "@modelcontextprotocol/core": [ "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" ], diff --git a/package.json b/package.json index 82dd50d74..3d37f5c70 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "node": ">=20", "pnpm": ">=10.24.0" }, - "packageManager": "pnpm@10.24.0", + "packageManager": "pnpm@10.26.1", "keywords": [ "modelcontextprotocol", "mcp" diff --git a/packages/client/src/client/sse.ts b/packages/client/src/client/sse.ts index bff74986e..4cfa77e17 100644 --- a/packages/client/src/client/sse.ts +++ b/packages/client/src/client/sse.ts @@ -1,6 +1,7 @@ import type { FetchLike, JSONRPCMessage, Transport } from '@modelcontextprotocol/core'; import { createFetchWithInit, JSONRPCMessageSchema, normalizeHeaders } from '@modelcontextprotocol/core'; -import { type ErrorEvent, EventSource, type EventSourceInit } from 'eventsource'; +import type { ErrorEvent, EventSourceInit } from 'eventsource'; +import { EventSource } from 'eventsource'; import type { AuthResult, OAuthClientProvider } from './auth.js'; import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js'; diff --git a/packages/server-express/README.md b/packages/server-express/README.md new file mode 100644 index 000000000..c1b10a9c7 --- /dev/null +++ b/packages/server-express/README.md @@ -0,0 +1,81 @@ +# `@modelcontextprotocol/server-express` + +Express adapters for the MCP TypeScript server SDK. + +This package is the Express-specific companion to [`@modelcontextprotocol/server`](../server/), which is framework-agnostic and uses Web Standard `Request`/`Response` interfaces. + +## Install + +```bash +npm install @modelcontextprotocol/server @modelcontextprotocol/server-express zod +``` + +## Exports + +- `createMcpExpressApp(options?)` +- `hostHeaderValidation(allowedHosts)` +- `localhostHostValidation()` +- `mcpAuthRouter(options)` +- `mcpAuthMetadataRouter(options)` +- `requireBearerAuth(options)` + +## Usage + +### Create an Express app with localhost DNS rebinding protection + +```ts +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; + +const app = createMcpExpressApp(); // default host is 127.0.0.1; protection enabled +``` + +### Streamable HTTP endpoint (Express) + +```ts +import { McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; + +const app = createMcpExpressApp(); + +app.post('/mcp', async (req, res) => { + const transport = new StreamableHTTPServerTransport(); + await transport.handleRequest(req, res, req.body); +}); +``` + +### OAuth routes (Express) + +`@modelcontextprotocol/server` provides Web-standard auth handlers; this package wraps them as Express routers. + +```ts +import { mcpAuthRouter } from '@modelcontextprotocol/server-express'; +import type { OAuthServerProvider } from '@modelcontextprotocol/server'; +import express from 'express'; + +const provider: OAuthServerProvider = /* ... */; +const app = express(); +app.use(express.json()); + +// MUST be mounted at the app root +app.use( + mcpAuthRouter({ + provider, + issuerUrl: new URL('https://auth.example.com') + }) +); +``` + +### Bearer auth middleware (Express) + +`requireBearerAuth` validates the `Authorization: Bearer ...` header and sets `req.auth` on success. + +```ts +import { requireBearerAuth } from '@modelcontextprotocol/server-express'; +import type { OAuthTokenVerifier } from '@modelcontextprotocol/server'; + +const verifier: OAuthTokenVerifier = /* ... */; + +app.post('/protected', requireBearerAuth({ verifier }), (req, res) => { + res.json({ clientId: req.auth?.clientId }); +}); +``` diff --git a/packages/server-express/eslint.config.mjs b/packages/server-express/eslint.config.mjs new file mode 100644 index 000000000..03d533134 --- /dev/null +++ b/packages/server-express/eslint.config.mjs @@ -0,0 +1,12 @@ +// @ts-check + +import baseConfig from '@modelcontextprotocol/eslint-config'; + +export default [ + ...baseConfig, + { + settings: { + 'import/internal-regex': '^@modelcontextprotocol/(server|core)' + } + } +]; diff --git a/packages/server-express/package.json b/packages/server-express/package.json new file mode 100644 index 000000000..d7d31cb1e --- /dev/null +++ b/packages/server-express/package.json @@ -0,0 +1,67 @@ +{ + "name": "@modelcontextprotocol/server-express", + "version": "2.0.0-alpha.0", + "description": "Express adapters for the Model Context Protocol TypeScript server SDK", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=20", + "pnpm": ">=10.24.0" + }, + "packageManager": "pnpm@10.24.0", + "keywords": [ + "modelcontextprotocol", + "mcp", + "express" + ], + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", + "build": "tsdown", + "build:watch": "tsdown --watch", + "prepack": "npm run build", + "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", + "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", + "check": "npm run typecheck && npm run lint", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@modelcontextprotocol/server": "workspace:^", + "express": "catalog:runtimeServerOnly" + }, + "devDependencies": { + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@modelcontextprotocol/eslint-config": "workspace:^", + "@eslint/js": "catalog:devTools", + "@types/express": "catalog:devTools", + "@types/express-serve-static-core": "catalog:devTools", + "@types/supertest": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", + "eslint": "catalog:devTools", + "eslint-config-prettier": "catalog:devTools", + "eslint-plugin-n": "catalog:devTools", + "prettier": "catalog:devTools", + "supertest": "catalog:devTools", + "tsdown": "catalog:devTools", + "typescript": "catalog:devTools", + "typescript-eslint": "catalog:devTools", + "vitest": "catalog:devTools" + } +} diff --git a/packages/server-express/src/auth/bearerAuth.ts b/packages/server-express/src/auth/bearerAuth.ts new file mode 100644 index 000000000..a923ff796 --- /dev/null +++ b/packages/server-express/src/auth/bearerAuth.ts @@ -0,0 +1,62 @@ +import { URL } from 'node:url'; + +import type { AuthInfo } from '@modelcontextprotocol/core'; +import type { BearerAuthMiddlewareOptions } from '@modelcontextprotocol/server'; +import { requireBearerAuth as requireBearerAuthWeb } from '@modelcontextprotocol/server'; +import type { NextFunction, Request as ExpressRequest, RequestHandler, Response as ExpressResponse } from 'express'; + +declare module 'express-serve-static-core' { + interface Request { + /** + * Information about the validated access token, if `requireBearerAuth` was used. + */ + auth?: AuthInfo; + } +} + +function expressRequestUrl(req: ExpressRequest): URL { + const host = req.get('host') ?? req.headers.host ?? 'localhost'; + const protocol = req.protocol ?? 'http'; + const path = req.originalUrl ?? req.url ?? '/'; + return new URL(path, `${protocol}://${host}`); +} + +async function writeWebResponse(res: ExpressResponse, webResponse: Response): Promise { + res.status(webResponse.status); + for (const [k, v] of webResponse.headers.entries()) { + res.setHeader(k, v); + } + const bodyText = await webResponse.text(); + res.send(bodyText); +} + +/** + * Express middleware wrapper for the Web-standard `requireBearerAuth` helper. + * + * On success, sets `req.auth` and calls `next()`. + * On failure, writes the JSON error response and ends the request. + */ +export function requireBearerAuth(options: BearerAuthMiddlewareOptions): RequestHandler { + return async (req, res, next: NextFunction) => { + try { + const url = expressRequestUrl(req); + const webReq = new Request(url, { + method: req.method, + headers: { + authorization: req.headers.authorization ?? '' + } + }); + + const result = await requireBearerAuthWeb(webReq, options); + if ('authInfo' in result) { + req.auth = result.authInfo; + next(); + return; + } + + await writeWebResponse(res, result.response); + } catch (err) { + next(err); + } + }; +} diff --git a/packages/server-express/src/auth/router.ts b/packages/server-express/src/auth/router.ts new file mode 100644 index 000000000..c50d2007d --- /dev/null +++ b/packages/server-express/src/auth/router.ts @@ -0,0 +1,172 @@ +import type { IncomingMessage } from 'node:http'; +import { Readable } from 'node:stream'; +import { URL } from 'node:url'; + +import type { AuthMetadataOptions, AuthRouterOptions, WebHandlerContext } from '@modelcontextprotocol/server'; +import { mcpAuthMetadataRouter as createWebAuthMetadataRouter, mcpAuthRouter as createWebAuthRouter } from '@modelcontextprotocol/server'; +import type { RequestHandler, Response as ExpressResponse } from 'express'; +import express from 'express'; + +type ExpressRequestLike = IncomingMessage & { + method: string; + headers: Record; + originalUrl?: string; + url?: string; + protocol?: string; + // express adds this when trust proxy is enabled + ip?: string; + body?: unknown; + get?: (name: string) => string | undefined; +}; + +function expressRequestUrl(req: ExpressRequestLike): URL { + const host = req.get?.('host') ?? req.headers.host ?? 'localhost'; + const proto = req.protocol ?? 'http'; + const path = req.originalUrl ?? req.url ?? '/'; + return new URL(path, `${proto}://${host}`); +} + +function toHeaders(req: ExpressRequestLike): Headers { + const headers = new Headers(); + for (const [key, value] of Object.entries(req.headers)) { + if (value === undefined) continue; + if (Array.isArray(value)) { + headers.set(key, value.join(', ')); + } else { + headers.set(key, value); + } + } + return headers; +} + +async function readBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks); +} + +async function expressToWebRequest(req: ExpressRequestLike, parsedBodyProvided: boolean): Promise { + const url = expressRequestUrl(req); + const headers = toHeaders(req); + + // If upstream body parsing ran, the Node stream is likely consumed. + if (parsedBodyProvided) { + return new Request(url, { method: req.method, headers }); + } + + if (req.method === 'GET' || req.method === 'HEAD') { + return new Request(url, { method: req.method, headers }); + } + + const body = await readBody(req); + return new Request(url, { method: req.method, headers, body }); +} + +async function writeWebResponse(res: ExpressResponse, webResponse: Response): Promise { + res.status(webResponse.status); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getSetCookie = (webResponse.headers as any).getSetCookie as (() => string[]) | undefined; + const setCookies = typeof getSetCookie === 'function' ? getSetCookie.call(webResponse.headers) : undefined; + + for (const [key, value] of webResponse.headers.entries()) { + if (key.toLowerCase() === 'set-cookie' && setCookies?.length) continue; + res.setHeader(key, value); + } + + if (setCookies?.length) { + res.setHeader('set-cookie', setCookies); + } + + res.flushHeaders?.(); + + if (!webResponse.body) { + res.end(); + return; + } + + await new Promise((resolve, reject) => { + const readable = Readable.fromWeb(webResponse.body as unknown as ReadableStream); + readable.on('error', err => { + try { + res.destroy(err as Error); + } catch { + // ignore + } + reject(err); + }); + res.on('error', reject); + res.on('close', () => { + try { + readable.destroy(); + } catch { + // ignore + } + }); + readable.pipe(res); + res.on('finish', () => resolve()); + }); +} + +function toHandlerContext(req: ExpressRequestLike): WebHandlerContext { + return { + parsedBody: req.body, + clientAddress: req.ip + }; +} + +/** + * Express router adapter for the Web-standard `mcpAuthRouter` from `@modelcontextprotocol/server`. + * + * IMPORTANT: This router MUST be mounted at the application root, like: + * + * ```ts + * app.use(mcpAuthRouter(...)) + * ``` + */ +export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler { + const web = createWebAuthRouter(options); + const router = express.Router(); + + for (const route of web.routes) { + router.all(route.path, async (req, res, next) => { + try { + const parsedBodyProvided = (req as ExpressRequestLike).body !== undefined; + const webReq = await expressToWebRequest(req as ExpressRequestLike, parsedBodyProvided); + const webRes = await route.handler(webReq, toHandlerContext(req as ExpressRequestLike)); + await writeWebResponse(res, webRes); + } catch (err) { + next(err); + } + }); + } + + return router; +} + +/** + * Express router adapter for the Web-standard `mcpAuthMetadataRouter` from `@modelcontextprotocol/server`. + * + * IMPORTANT: This router MUST be mounted at the application root. + */ +export function mcpAuthMetadataRouter(options: AuthMetadataOptions): RequestHandler { + const web = createWebAuthMetadataRouter(options); + const router = express.Router(); + + for (const route of web.routes) { + router.all(route.path, async (req, res, next) => { + try { + const parsedBodyProvided = (req as ExpressRequestLike).body !== undefined; + const webReq = await expressToWebRequest(req as ExpressRequestLike, parsedBodyProvided); + const webRes = await route.handler(webReq, toHandlerContext(req as ExpressRequestLike)); + await writeWebResponse(res, webRes); + } catch (err) { + next(err); + } + }); + } + + return router; +} diff --git a/packages/server/src/server/express.ts b/packages/server-express/src/express.ts similarity index 100% rename from packages/server/src/server/express.ts rename to packages/server-express/src/express.ts diff --git a/packages/server-express/src/index.ts b/packages/server-express/src/index.ts new file mode 100644 index 000000000..3c5b72fe7 --- /dev/null +++ b/packages/server-express/src/index.ts @@ -0,0 +1,4 @@ +export * from './auth/bearerAuth.js'; +export * from './auth/router.js'; +export * from './express.js'; +export * from './middleware/hostHeaderValidation.js'; diff --git a/packages/server-express/src/middleware/hostHeaderValidation.ts b/packages/server-express/src/middleware/hostHeaderValidation.ts new file mode 100644 index 000000000..00ee74e1f --- /dev/null +++ b/packages/server-express/src/middleware/hostHeaderValidation.ts @@ -0,0 +1,52 @@ +import { localhostAllowedHostnames, validateHostHeader } from '@modelcontextprotocol/server'; +import type { NextFunction, Request, RequestHandler, Response } from 'express'; + +/** + * Express middleware for DNS rebinding protection. + * Validates Host header hostname (port-agnostic) against an allowed list. + * + * This is particularly important for servers without authorization or HTTPS, + * such as localhost servers or development servers. DNS rebinding attacks can + * bypass same-origin policy by manipulating DNS to point a domain to a + * localhost address, allowing malicious websites to access your local server. + * + * @param allowedHostnames - List of allowed hostnames (without ports). + * For IPv6, provide the address with brackets (e.g., '[::1]'). + * @returns Express middleware function + * + * @example + * ```typescript + * const middleware = hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); + * app.use(middleware); + * ``` + */ +export function hostHeaderValidation(allowedHostnames: string[]): RequestHandler { + return (req: Request, res: Response, next: NextFunction) => { + const result = validateHostHeader(req.headers.host, allowedHostnames); + if (!result.ok) { + res.status(403).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: result.message + }, + id: null + }); + return; + } + next(); + }; +} + +/** + * Convenience middleware for localhost DNS rebinding protection. + * Allows only localhost, 127.0.0.1, and [::1] (IPv6 localhost) hostnames. + * + * @example + * ```typescript + * app.use(localhostHostValidation()); + * ``` + */ +export function localhostHostValidation(): RequestHandler { + return hostHeaderValidation(localhostAllowedHostnames()); +} diff --git a/packages/server/test/server/auth/router.test.ts b/packages/server-express/test/server/auth/router.test.ts similarity index 95% rename from packages/server/test/server/auth/router.test.ts rename to packages/server-express/test/server/auth/router.test.ts index 250fca4c4..7a6c09690 100644 --- a/packages/server/test/server/auth/router.test.ts +++ b/packages/server-express/test/server/auth/router.test.ts @@ -1,14 +1,18 @@ -import type { OAuthClientInformationFull, OAuthMetadata, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; -import type { AuthInfo } from '@modelcontextprotocol/core'; -import { InvalidTokenError } from '@modelcontextprotocol/core'; -import type { Response } from 'express'; +import type { + AuthInfo, + OAuthClientInformationFull, + OAuthMetadata, + OAuthTokenRevocationRequest, + OAuthTokens +} from '@modelcontextprotocol/server'; +import { InvalidTokenError } from '@modelcontextprotocol/server'; import express from 'express'; import supertest from 'supertest'; -import type { OAuthRegisteredClientsStore } from '../../../src/server/auth/clients.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../../../src/server/auth/provider.js'; -import type { AuthMetadataOptions, AuthRouterOptions } from '../../../src/server/auth/router.js'; -import { mcpAuthMetadataRouter, mcpAuthRouter } from '../../../src/server/auth/router.js'; +import type { OAuthRegisteredClientsStore } from '@modelcontextprotocol/server'; +import type { AuthorizationParams, OAuthServerProvider } from '@modelcontextprotocol/server'; +import type { AuthMetadataOptions, AuthRouterOptions } from '@modelcontextprotocol/server'; +import { mcpAuthMetadataRouter, mcpAuthRouter } from '../../../src/auth/router.js'; describe('MCP Auth Router', () => { // Setup mock provider with full capabilities @@ -32,13 +36,13 @@ describe('MCP Auth Router', () => { const mockProvider: OAuthServerProvider = { clientsStore: mockClientStore, - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise { + async authorize(_client: OAuthClientInformationFull, params: AuthorizationParams): Promise { const redirectUrl = new URL(params.redirectUri); redirectUrl.searchParams.set('code', 'mock_auth_code'); if (params.state) { redirectUrl.searchParams.set('state', params.state); } - res.redirect(302, redirectUrl.toString()); + return Response.redirect(redirectUrl.toString(), 302); }, async challengeForAuthorizationCode(): Promise { @@ -95,13 +99,13 @@ describe('MCP Auth Router', () => { } }, - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise { + async authorize(_client: OAuthClientInformationFull, params: AuthorizationParams): Promise { const redirectUrl = new URL(params.redirectUri); redirectUrl.searchParams.set('code', 'mock_auth_code'); if (params.state) { redirectUrl.searchParams.set('state', params.state); } - res.redirect(302, redirectUrl.toString()); + return Response.redirect(redirectUrl.toString(), 302); }, async challengeForAuthorizationCode(): Promise { diff --git a/packages/server-express/tsconfig.json b/packages/server-express/tsconfig.json new file mode 100644 index 000000000..0d7fdd0c0 --- /dev/null +++ b/packages/server-express/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "*": ["./*"], + "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/core": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + ] + } + } +} diff --git a/packages/server-express/tsdown.config.ts b/packages/server-express/tsdown.config.ts new file mode 100644 index 000000000..c72e7a2c4 --- /dev/null +++ b/packages/server-express/tsdown.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + outDir: 'dist', + clean: true, + sourcemap: true, + target: 'esnext', + platform: 'node', + shims: true, + dts: { + resolver: 'tsc', + compilerOptions: { + baseUrl: '.', + paths: { + '@modelcontextprotocol/server': ['../server/src/index.ts'], + '@modelcontextprotocol/core': ['../core/src/index.ts'] + } + } + }, + noExternal: ['@modelcontextprotocol/server', '@modelcontextprotocol/core'] +}); diff --git a/packages/server-express/vitest.config.js b/packages/server-express/vitest.config.js new file mode 100644 index 000000000..496fca320 --- /dev/null +++ b/packages/server-express/vitest.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; diff --git a/packages/server-hono/README.md b/packages/server-hono/README.md new file mode 100644 index 000000000..d2788881a --- /dev/null +++ b/packages/server-hono/README.md @@ -0,0 +1,64 @@ +# `@modelcontextprotocol/server-hono` + +Hono adapters for the MCP TypeScript server SDK. + +This package is the Hono-specific companion to [`@modelcontextprotocol/server`](../server/), which is framework-agnostic and uses Web Standard `Request`/`Response` interfaces. + +## Install + +```bash +npm install @modelcontextprotocol/server @modelcontextprotocol/server-hono hono zod +``` + +## Exports + +- `mcpStreamableHttpHandler(transport)` +- `registerMcpAuthRoutes(app, options)` +- `registerMcpAuthMetadataRoutes(app, options)` +- `hostHeaderValidation(allowedHosts)` +- `localhostHostValidation()` + +## Usage + +### Streamable HTTP endpoint (Hono) + +```ts +import { Hono } from 'hono'; +import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { mcpStreamableHttpHandler } from '@modelcontextprotocol/server-hono'; + +const server = new McpServer({ name: 'my-server', version: '1.0.0' }); +const transport = new WebStandardStreamableHTTPServerTransport(); +await server.connect(transport); + +const app = new Hono(); +app.all('/mcp', mcpStreamableHttpHandler(transport)); +``` + +### OAuth routes (Hono) + +`@modelcontextprotocol/server` provides Web-standard auth handlers; this package mounts them onto a Hono app. + +```ts +import { Hono } from 'hono'; +import type { OAuthServerProvider } from '@modelcontextprotocol/server'; +import { registerMcpAuthRoutes } from '@modelcontextprotocol/server-hono'; + +const provider: OAuthServerProvider = /* ... */; + +const app = new Hono(); +registerMcpAuthRoutes(app, { + provider, + issuerUrl: new URL('https://auth.example.com') +}); +``` + +### Host header validation (DNS rebinding protection) + +```ts +import { Hono } from 'hono'; +import { localhostHostValidation } from '@modelcontextprotocol/server-hono'; + +const app = new Hono(); +app.use('*', localhostHostValidation()); +``` diff --git a/packages/server-hono/eslint.config.mjs b/packages/server-hono/eslint.config.mjs new file mode 100644 index 000000000..03d533134 --- /dev/null +++ b/packages/server-hono/eslint.config.mjs @@ -0,0 +1,12 @@ +// @ts-check + +import baseConfig from '@modelcontextprotocol/eslint-config'; + +export default [ + ...baseConfig, + { + settings: { + 'import/internal-regex': '^@modelcontextprotocol/(server|core)' + } + } +]; diff --git a/packages/server-hono/package.json b/packages/server-hono/package.json new file mode 100644 index 000000000..33f633d40 --- /dev/null +++ b/packages/server-hono/package.json @@ -0,0 +1,63 @@ +{ + "name": "@modelcontextprotocol/server-hono", + "version": "2.0.0-alpha.0", + "description": "Hono adapters for the Model Context Protocol TypeScript server SDK", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=20", + "pnpm": ">=10.24.0" + }, + "packageManager": "pnpm@10.24.0", + "keywords": [ + "modelcontextprotocol", + "mcp", + "hono" + ], + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", + "build": "tsdown", + "build:watch": "tsdown --watch", + "prepack": "npm run build", + "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", + "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", + "check": "npm run typecheck && npm run lint", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@modelcontextprotocol/server": "workspace:^", + "hono": "catalog:runtimeServerOnly" + }, + "devDependencies": { + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@modelcontextprotocol/eslint-config": "workspace:^", + "@eslint/js": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", + "eslint": "catalog:devTools", + "eslint-config-prettier": "catalog:devTools", + "eslint-plugin-n": "catalog:devTools", + "prettier": "catalog:devTools", + "tsdown": "catalog:devTools", + "typescript": "catalog:devTools", + "typescript-eslint": "catalog:devTools", + "vitest": "catalog:devTools" + } +} diff --git a/packages/server-hono/src/auth/router.ts b/packages/server-hono/src/auth/router.ts new file mode 100644 index 000000000..4c61c1d2c --- /dev/null +++ b/packages/server-hono/src/auth/router.ts @@ -0,0 +1,33 @@ +import type { AuthMetadataOptions, AuthRoute, AuthRouterOptions } from '@modelcontextprotocol/server'; +import { mcpAuthMetadataRouter as createWebAuthMetadataRouter, mcpAuthRouter as createWebAuthRouter } from '@modelcontextprotocol/server'; +import type { Handler, Hono } from 'hono'; + +export type RegisterMcpAuthRoutesOptions = AuthRouterOptions; + +/** + * Registers the standard MCP OAuth endpoints on a Hono app. + * + * IMPORTANT: These routes MUST be mounted at the application root. + */ +export function registerMcpAuthRoutes(app: Hono, options: RegisterMcpAuthRoutesOptions): void { + const web = createWebAuthRouter(options); + registerRoutes(app, web.routes); +} + +/** + * Registers only the auth metadata endpoints (RFC 8414 + RFC 9728) on a Hono app. + * + * IMPORTANT: These routes MUST be mounted at the application root. + */ +export function registerMcpAuthMetadataRoutes(app: Hono, options: AuthMetadataOptions): void { + const web = createWebAuthMetadataRouter(options); + registerRoutes(app, web.routes); +} + +function registerRoutes(app: Hono, routes: AuthRoute[]): void { + for (const route of routes) { + // Hono's `on()` expects methods like 'GET', 'POST', etc. + const handler: Handler = c => route.handler(c.req.raw); + app.on(route.methods, route.path, handler); + } +} diff --git a/packages/server-hono/src/index.ts b/packages/server-hono/src/index.ts new file mode 100644 index 000000000..5a7cb5129 --- /dev/null +++ b/packages/server-hono/src/index.ts @@ -0,0 +1,3 @@ +export * from './auth/router.js'; +export * from './middleware/hostHeaderValidation.js'; +export * from './streamableHttp.js'; diff --git a/packages/server-hono/src/middleware/hostHeaderValidation.ts b/packages/server-hono/src/middleware/hostHeaderValidation.ts new file mode 100644 index 000000000..8f7b20e88 --- /dev/null +++ b/packages/server-hono/src/middleware/hostHeaderValidation.ts @@ -0,0 +1,33 @@ +import { localhostAllowedHostnames, validateHostHeader } from '@modelcontextprotocol/server'; +import type { MiddlewareHandler } from 'hono'; + +/** + * Hono middleware for DNS rebinding protection. + * Validates Host header hostname (port-agnostic) against an allowed list. + */ +export function hostHeaderValidation(allowedHostnames: string[]): MiddlewareHandler { + return async (c, next) => { + const result = validateHostHeader(c.req.header('host'), allowedHostnames); + if (!result.ok) { + return c.json( + { + jsonrpc: '2.0', + error: { + code: -32000, + message: result.message + }, + id: null + }, + 403 + ); + } + return await next(); + }; +} + +/** + * Convenience middleware for localhost DNS rebinding protection. + */ +export function localhostHostValidation(): MiddlewareHandler { + return hostHeaderValidation(localhostAllowedHostnames()); +} diff --git a/packages/server-hono/src/streamableHttp.ts b/packages/server-hono/src/streamableHttp.ts new file mode 100644 index 000000000..d81960713 --- /dev/null +++ b/packages/server-hono/src/streamableHttp.ts @@ -0,0 +1,14 @@ +import type { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import type { Context, Handler } from 'hono'; + +/** + * Convenience Hono handler for the WebStandard Streamable HTTP transport. + * + * Usage: + * ```ts + * app.all('/mcp', mcpStreamableHttpHandler(transport)) + * ``` + */ +export function mcpStreamableHttpHandler(transport: WebStandardStreamableHTTPServerTransport): Handler { + return (c: Context) => transport.handleRequest(c.req.raw); +} diff --git a/packages/server-hono/test/server-hono.test.ts b/packages/server-hono/test/server-hono.test.ts new file mode 100644 index 000000000..8b143411b --- /dev/null +++ b/packages/server-hono/test/server-hono.test.ts @@ -0,0 +1,114 @@ +import type { AuthorizationParams, OAuthClientInformationFull, OAuthServerProvider, OAuthTokens } from '@modelcontextprotocol/server'; +import { Hono } from 'hono'; + +import { registerMcpAuthRoutes } from '../src/auth/router.js'; +import { hostHeaderValidation } from '../src/middleware/hostHeaderValidation.js'; +import { mcpStreamableHttpHandler } from '../src/streamableHttp.js'; + +describe('@modelcontextprotocol/server-hono', () => { + test('mcpStreamableHttpHandler delegates to transport.handleRequest', async () => { + const calls: { url?: string; method?: string }[] = []; + + const transport = { + async handleRequest(req: Request): Promise { + calls.push({ url: req.url, method: req.method }); + return new Response('ok', { status: 200, headers: { 'content-type': 'text/plain' } }); + } + }; + + const app = new Hono(); + app.all('/mcp', mcpStreamableHttpHandler(transport as unknown as Parameters[0])); + + const res = await app.request('http://localhost/mcp', { method: 'POST' }); + expect(res.status).toBe(200); + expect(await res.text()).toBe('ok'); + expect(calls).toHaveLength(1); + expect(calls[0]!.method).toBe('POST'); + expect(calls[0]!.url).toBe('http://localhost/mcp'); + }); + + test('hostHeaderValidation blocks invalid Host and allows valid Host', async () => { + const app = new Hono(); + app.use('*', hostHeaderValidation(['localhost'])); + app.get('/health', c => c.text('ok')); + + const bad = await app.request('http://localhost/health', { headers: { Host: 'evil.com:3000' } }); + expect(bad.status).toBe(403); + expect(await bad.json()).toEqual( + expect.objectContaining({ + jsonrpc: '2.0', + error: expect.objectContaining({ + code: -32000 + }), + id: null + }) + ); + + const good = await app.request('http://localhost/health', { headers: { Host: 'localhost:3000' } }); + expect(good.status).toBe(200); + expect(await good.text()).toBe('ok'); + }); + + test('registerMcpAuthRoutes mounts metadata + authorize routes', async () => { + const validClient: OAuthClientInformationFull = { + client_id: 'valid-client', + client_secret: 'valid-secret', + redirect_uris: ['https://example.com/callback'] + }; + + const provider: OAuthServerProvider = { + clientsStore: { + async getClient(clientId: string) { + return clientId === 'valid-client' ? validClient : undefined; + } + }, + async authorize(_client: OAuthClientInformationFull, params: AuthorizationParams): Promise { + const u = new URL(params.redirectUri); + u.searchParams.set('code', 'mock_auth_code'); + if (params.state) u.searchParams.set('state', params.state); + return Response.redirect(u.toString(), 302); + }, + async challengeForAuthorizationCode(): Promise { + return 'mock_challenge'; + }, + async exchangeAuthorizationCode(): Promise { + return { + access_token: 'mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'mock_refresh_token' + }; + }, + async exchangeRefreshToken(): Promise { + return { + access_token: 'new_mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'new_mock_refresh_token' + }; + }, + async verifyAccessToken() { + throw new Error('not used'); + } + }; + + const app = new Hono(); + registerMcpAuthRoutes(app, { provider, issuerUrl: new URL('https://auth.example.com') }); + + const metadata = await app.request('http://localhost/.well-known/oauth-authorization-server', { method: 'GET' }); + expect(metadata.status).toBe(200); + const metaJson = (await metadata.json()) as { issuer?: string; authorization_endpoint?: string }; + expect(metaJson.issuer).toBe('https://auth.example.com/'); + expect(metaJson.authorization_endpoint).toBe('https://auth.example.com/authorize'); + + const authorize = await app.request( + 'http://localhost/authorize?client_id=valid-client&response_type=code&code_challenge=x&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&state=s', + { method: 'GET' } + ); + expect(authorize.status).toBe(302); + const location = authorize.headers.get('location')!; + expect(location).toContain('https://example.com/callback'); + expect(location).toContain('code=mock_auth_code'); + expect(location).toContain('state=s'); + }); +}); diff --git a/packages/server-hono/tsconfig.json b/packages/server-hono/tsconfig.json new file mode 100644 index 000000000..0d7fdd0c0 --- /dev/null +++ b/packages/server-hono/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "*": ["./*"], + "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/core": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + ] + } + } +} diff --git a/packages/server-hono/tsdown.config.ts b/packages/server-hono/tsdown.config.ts new file mode 100644 index 000000000..c72e7a2c4 --- /dev/null +++ b/packages/server-hono/tsdown.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + outDir: 'dist', + clean: true, + sourcemap: true, + target: 'esnext', + platform: 'node', + shims: true, + dts: { + resolver: 'tsc', + compilerOptions: { + baseUrl: '.', + paths: { + '@modelcontextprotocol/server': ['../server/src/index.ts'], + '@modelcontextprotocol/core': ['../core/src/index.ts'] + } + } + }, + noExternal: ['@modelcontextprotocol/server', '@modelcontextprotocol/core'] +}); diff --git a/packages/server-hono/vitest.config.js b/packages/server-hono/vitest.config.js new file mode 100644 index 000000000..496fca320 --- /dev/null +++ b/packages/server-hono/vitest.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; diff --git a/packages/server/package.json b/packages/server/package.json index b039751f6..4f32cf171 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,11 +45,6 @@ }, "dependencies": { "content-type": "catalog:runtimeServerOnly", - "cors": "catalog:runtimeServerOnly", - "@hono/node-server": "catalog:runtimeServerOnly", - "hono": "catalog:runtimeServerOnly", - "express": "catalog:runtimeServerOnly", - "express-rate-limit": "catalog:runtimeServerOnly", "raw-body": "catalog:runtimeServerOnly", "pkce-challenge": "catalog:runtimeShared", "zod": "catalog:runtimeShared", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 8c9b9af5f..bbf3c2f59 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,7 +1,7 @@ export * from './server/completable.js'; -export * from './server/express.js'; export * from './server/inMemoryEventStore.js'; export * from './server/mcp.js'; +export * from './server/middleware/hostHeaderValidation.js'; export * from './server/server.js'; export * from './server/sse.js'; export * from './server/stdio.js'; diff --git a/packages/server/src/server/auth/handlers/authorize.ts b/packages/server/src/server/auth/handlers/authorize.ts index 65875529e..df2702f88 100644 --- a/packages/server/src/server/auth/handlers/authorize.ts +++ b/packages/server/src/server/auth/handlers/authorize.ts @@ -1,12 +1,9 @@ import { InvalidClientError, InvalidRequestError, OAuthError, ServerError, TooManyRequestsError } from '@modelcontextprotocol/core'; -import type { RequestHandler } from 'express'; -import express from 'express'; -import type { Options as RateLimitOptions } from 'express-rate-limit'; -import { rateLimit } from 'express-rate-limit'; import * as z from 'zod/v4'; -import { allowedMethods } from '../middleware/allowedMethods.js'; import type { OAuthServerProvider } from '../provider.js'; +import type { WebHandler } from '../web.js'; +import { getClientAddress, getParsedBody, InMemoryRateLimiter, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; export type AuthorizationHandlerOptions = { provider: OAuthServerProvider; @@ -14,7 +11,7 @@ export type AuthorizationHandlerOptions = { * Rate limiting configuration for the authorization endpoint. * Set to false to disable rate limiting for this endpoint. */ - rateLimit?: Partial | false; + rateLimit?: Partial<{ windowMs: number; max: number }> | false; }; // Parameters that must be validated in order to issue redirects. @@ -36,28 +33,44 @@ const RequestAuthorizationParamsSchema = z.object({ resource: z.string().url().optional() }); -export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): RequestHandler { - // Create a router to apply middleware - const router = express.Router(); - router.use(allowedMethods(['GET', 'POST'])); - router.use(express.urlencoded({ extended: false })); - - // Apply rate limiting unless explicitly disabled - if (rateLimitConfig !== false) { - router.use( - rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // 100 requests per windowMs - standardHeaders: true, - legacyHeaders: false, - message: new TooManyRequestsError('You have exceeded the rate limit for authorization requests').toResponseObject(), - ...rateLimitConfig - }) - ); - } +export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): WebHandler { + const limiter = + rateLimitConfig === false + ? undefined + : new InMemoryRateLimiter({ + windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1000, + max: rateLimitConfig?.max ?? 100 + }); + + return async (req, ctx) => { + const noStore = noStoreHeaders(); + + // Rate limit by client address where possible (best-effort). + if (limiter) { + const key = `${getClientAddress(req, ctx) ?? 'global'}:authorize`; + const rl = limiter.consume(key); + if (!rl.allowed) { + return jsonResponse( + new TooManyRequestsError('You have exceeded the rate limit for authorization requests').toResponseObject(), + { + status: 429, + headers: { + ...noStore, + ...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {}) + } + } + ); + } + } - router.all('/', async (req, res) => { - res.setHeader('Cache-Control', 'no-store'); + if (req.method !== 'GET' && req.method !== 'POST') { + const resp = methodNotAllowedResponse(req, ['GET', 'POST']); + const body = await resp.text(); + return new Response(body, { + status: resp.status, + headers: { ...Object.fromEntries(resp.headers.entries()), ...noStore } + }); + } // In the authorization flow, errors are split into two categories: // 1. Pre-redirect errors (direct response with 400) @@ -66,7 +79,9 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A // Phase 1: Validate client_id and redirect_uri. Any errors here must be direct responses. let client_id, redirect_uri, client; try { - const result = ClientAuthorizationParamsSchema.safeParse(req.method === 'POST' ? req.body : req.query); + const source = + req.method === 'POST' ? await getParsedBody(req, ctx) : Object.fromEntries(new URL(req.url).searchParams.entries()); + const result = ClientAuthorizationParamsSchema.safeParse(source); if (!result.success) { throw new InvalidRequestError(result.error.message); } @@ -97,20 +112,20 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A // user anyway. if (error instanceof OAuthError) { const status = error instanceof ServerError ? 500 : 400; - res.status(status).json(error.toResponseObject()); + return jsonResponse(error.toResponseObject(), { status, headers: noStore }); } else { const serverError = new ServerError('Internal Server Error'); - res.status(500).json(serverError.toResponseObject()); + return jsonResponse(serverError.toResponseObject(), { status: 500, headers: noStore }); } - - return; } // Phase 2: Validate other parameters. Any errors here should go into redirect responses. let state; try { // Parse and validate authorization parameters - const parseResult = RequestAuthorizationParamsSchema.safeParse(req.method === 'POST' ? req.body : req.query); + const source = + req.method === 'POST' ? await getParsedBody(req, ctx) : Object.fromEntries(new URL(req.url).searchParams.entries()); + const parseResult = RequestAuthorizationParamsSchema.safeParse(source); if (!parseResult.success) { throw new InvalidRequestError(parseResult.error.message); } @@ -125,29 +140,28 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A } // All validation passed, proceed with authorization - await provider.authorize( - client, - { - state, - scopes: requestedScopes, - redirectUri: redirect_uri!, // TODO: Someone to look at. Strict tsconfig showed this could be undefined, while the return type is string. - codeChallenge: code_challenge, - resource: resource ? new URL(resource) : undefined - }, - res - ); + const providerResponse = await provider.authorize(client, { + state, + scopes: requestedScopes, + redirectUri: redirect_uri!, // TODO: Someone to look at. Strict tsconfig showed this could be undefined, while the return type is string. + codeChallenge: code_challenge, + resource: resource ? new URL(resource) : undefined + }); + const headers = new Headers(providerResponse.headers); + headers.set('Cache-Control', 'no-store'); + return new Response(providerResponse.body, { status: providerResponse.status, headers }); } catch (error) { // Post-redirect errors - redirect with error parameters if (error instanceof OAuthError) { - res.redirect(302, createErrorRedirect(redirect_uri!, error, state)); + const location = createErrorRedirect(redirect_uri!, error, state); + return new Response(null, { status: 302, headers: { Location: location, ...noStore } }); } else { const serverError = new ServerError('Internal Server Error'); - res.redirect(302, createErrorRedirect(redirect_uri!, serverError, state)); + const location = createErrorRedirect(redirect_uri!, serverError, state); + return new Response(null, { status: 302, headers: { Location: location, ...noStore } }); } } - }); - - return router; + }; } /** diff --git a/packages/server/src/server/auth/handlers/metadata.ts b/packages/server/src/server/auth/handlers/metadata.ts index 529a6e57a..fea42a8cb 100644 --- a/packages/server/src/server/auth/handlers/metadata.ts +++ b/packages/server/src/server/auth/handlers/metadata.ts @@ -1,21 +1,33 @@ import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/core'; -import cors from 'cors'; -import type { RequestHandler } from 'express'; -import express from 'express'; -import { allowedMethods } from '../middleware/allowedMethods.js'; +import type { WebHandler } from '../web.js'; +import { corsHeaders, corsPreflightResponse, jsonResponse, methodNotAllowedResponse } from '../web.js'; -export function metadataHandler(metadata: OAuthMetadata | OAuthProtectedResourceMetadata): RequestHandler { - // Nested router so we can configure middleware and restrict HTTP method - const router = express.Router(); +export function metadataHandler(metadata: OAuthMetadata | OAuthProtectedResourceMetadata): WebHandler { + const cors = { + allowOrigin: '*', + allowMethods: ['GET', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization'], + maxAgeSeconds: 60 * 60 * 24 + } as const; - // Configure CORS to allow any origin, to make accessible to web-based MCP clients - router.use(cors()); + return async req => { + if (req.method === 'OPTIONS') { + return corsPreflightResponse(cors); + } + if (req.method !== 'GET') { + const resp = methodNotAllowedResponse(req, ['GET', 'OPTIONS']); + // Add CORS headers for consistency with successful responses. + const body = await resp.text(); + return new Response(body, { + status: resp.status, + headers: { ...Object.fromEntries(resp.headers.entries()), ...corsHeaders(cors) } + }); + } - router.use(allowedMethods(['GET', 'OPTIONS'])); - router.get('/', (req, res) => { - res.status(200).json(metadata); - }); - - return router; + return jsonResponse(metadata, { + status: 200, + headers: corsHeaders(cors) + }); + }; } diff --git a/packages/server/src/server/auth/handlers/register.ts b/packages/server/src/server/auth/handlers/register.ts index a78154d48..fa54644c3 100644 --- a/packages/server/src/server/auth/handlers/register.ts +++ b/packages/server/src/server/auth/handlers/register.ts @@ -8,14 +8,19 @@ import { ServerError, TooManyRequestsError } from '@modelcontextprotocol/core'; -import cors from 'cors'; -import type { RequestHandler } from 'express'; -import express from 'express'; -import type { Options as RateLimitOptions } from 'express-rate-limit'; -import { rateLimit } from 'express-rate-limit'; import type { OAuthRegisteredClientsStore } from '../clients.js'; -import { allowedMethods } from '../middleware/allowedMethods.js'; +import type { WebHandler } from '../web.js'; +import { + corsHeaders, + corsPreflightResponse, + getClientAddress, + getParsedBody, + InMemoryRateLimiter, + jsonResponse, + methodNotAllowedResponse, + noStoreHeaders +} from '../web.js'; export type ClientRegistrationHandlerOptions = { /** @@ -35,7 +40,7 @@ export type ClientRegistrationHandlerOptions = { * Set to false to disable rate limiting for this endpoint. * Registration endpoints are particularly sensitive to abuse and should be rate limited. */ - rateLimit?: Partial | false; + rateLimit?: Partial<{ windowMs: number; max: number }> | false; /** * Whether to generate a client ID before calling the client registration endpoint. @@ -52,39 +57,61 @@ export function clientRegistrationHandler({ clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS, rateLimit: rateLimitConfig, clientIdGeneration = true -}: ClientRegistrationHandlerOptions): RequestHandler { +}: ClientRegistrationHandlerOptions): WebHandler { if (!clientsStore.registerClient) { throw new Error('Client registration store does not support registering clients'); } - // Nested router so we can configure middleware and restrict HTTP method - const router = express.Router(); - - // Configure CORS to allow any origin, to make accessible to web-based MCP clients - router.use(cors()); - - router.use(allowedMethods(['POST'])); - router.use(express.json()); - - // Apply rate limiting unless explicitly disabled - stricter limits for registration - if (rateLimitConfig !== false) { - router.use( - rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour - max: 20, // 20 requests per hour - stricter as registration is sensitive - standardHeaders: true, - legacyHeaders: false, - message: new TooManyRequestsError('You have exceeded the rate limit for client registration requests').toResponseObject(), - ...rateLimitConfig - }) - ); - } + const limiter = + rateLimitConfig === false + ? undefined + : new InMemoryRateLimiter({ + windowMs: rateLimitConfig?.windowMs ?? 60 * 60 * 1000, + max: rateLimitConfig?.max ?? 20 + }); + + const cors = { + allowOrigin: '*', + allowMethods: ['POST', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization'], + maxAgeSeconds: 60 * 60 * 24 + } as const; + + return async (req, ctx) => { + const baseHeaders = { ...corsHeaders(cors), ...noStoreHeaders() }; + + if (req.method === 'OPTIONS') { + return corsPreflightResponse(cors); + } + if (req.method !== 'POST') { + const resp = methodNotAllowedResponse(req, ['POST', 'OPTIONS']); + const body = await resp.text(); + return new Response(body, { + status: resp.status, + headers: { ...Object.fromEntries(resp.headers.entries()), ...baseHeaders } + }); + } - router.post('/', async (req, res) => { - res.setHeader('Cache-Control', 'no-store'); + if (limiter) { + const key = `${getClientAddress(req, ctx) ?? 'global'}:register`; + const rl = limiter.consume(key); + if (!rl.allowed) { + return jsonResponse( + new TooManyRequestsError('You have exceeded the rate limit for client registration requests').toResponseObject(), + { + status: 429, + headers: { + ...baseHeaders, + ...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {}) + } + } + ); + } + } try { - const parseResult = OAuthClientMetadataSchema.safeParse(req.body); + const rawBody = await getParsedBody(req, ctx); + const parseResult = OAuthClientMetadataSchema.safeParse(rawBody); if (!parseResult.success) { throw new InvalidClientMetadataError(parseResult.error.message); } @@ -113,17 +140,14 @@ export function clientRegistrationHandler({ } clientInfo = await clientsStore.registerClient!(clientInfo); - res.status(201).json(clientInfo); + return jsonResponse(clientInfo, { status: 201, headers: baseHeaders }); } catch (error) { if (error instanceof OAuthError) { const status = error instanceof ServerError ? 500 : 400; - res.status(status).json(error.toResponseObject()); - } else { - const serverError = new ServerError('Internal Server Error'); - res.status(500).json(serverError.toResponseObject()); + return jsonResponse(error.toResponseObject(), { status, headers: baseHeaders }); } + const serverError = new ServerError('Internal Server Error'); + return jsonResponse(serverError.toResponseObject(), { status: 500, headers: baseHeaders }); } - }); - - return router; + }; } diff --git a/packages/server/src/server/auth/handlers/revoke.ts b/packages/server/src/server/auth/handlers/revoke.ts index c7c9f8a6a..30c611cff 100644 --- a/packages/server/src/server/auth/handlers/revoke.ts +++ b/packages/server/src/server/auth/handlers/revoke.ts @@ -5,15 +5,20 @@ import { ServerError, TooManyRequestsError } from '@modelcontextprotocol/core'; -import cors from 'cors'; -import type { RequestHandler } from 'express'; -import express from 'express'; -import type { Options as RateLimitOptions } from 'express-rate-limit'; -import { rateLimit } from 'express-rate-limit'; -import { allowedMethods } from '../middleware/allowedMethods.js'; import { authenticateClient } from '../middleware/clientAuth.js'; import type { OAuthServerProvider } from '../provider.js'; +import type { WebHandler } from '../web.js'; +import { + corsHeaders, + corsPreflightResponse, + getClientAddress, + getParsedBody, + InMemoryRateLimiter, + jsonResponse, + methodNotAllowedResponse, + noStoreHeaders +} from '../web.js'; export type RevocationHandlerOptions = { provider: OAuthServerProvider; @@ -21,67 +26,79 @@ export type RevocationHandlerOptions = { * Rate limiting configuration for the token revocation endpoint. * Set to false to disable rate limiting for this endpoint. */ - rateLimit?: Partial | false; + rateLimit?: Partial<{ windowMs: number; max: number }> | false; }; -export function revocationHandler({ provider, rateLimit: rateLimitConfig }: RevocationHandlerOptions): RequestHandler { +export function revocationHandler({ provider, rateLimit: rateLimitConfig }: RevocationHandlerOptions): WebHandler { if (!provider.revokeToken) { throw new Error('Auth provider does not support revoking tokens'); } - // Nested router so we can configure middleware and restrict HTTP method - const router = express.Router(); - - // Configure CORS to allow any origin, to make accessible to web-based MCP clients - router.use(cors()); + const limiter = + rateLimitConfig === false + ? undefined + : new InMemoryRateLimiter({ + windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1000, + max: rateLimitConfig?.max ?? 50 + }); - router.use(allowedMethods(['POST'])); - router.use(express.urlencoded({ extended: false })); + const cors = { + allowOrigin: '*', + allowMethods: ['POST', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization'], + maxAgeSeconds: 60 * 60 * 24 + } as const; - // Apply rate limiting unless explicitly disabled - if (rateLimitConfig !== false) { - router.use( - rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 50, // 50 requests per windowMs - standardHeaders: true, - legacyHeaders: false, - message: new TooManyRequestsError('You have exceeded the rate limit for token revocation requests').toResponseObject(), - ...rateLimitConfig - }) - ); - } + return async (req, ctx) => { + const baseHeaders = { ...corsHeaders(cors), ...noStoreHeaders() }; - // Authenticate and extract client details - router.use(authenticateClient({ clientsStore: provider.clientsStore })); + if (req.method === 'OPTIONS') { + return corsPreflightResponse(cors); + } + if (req.method !== 'POST') { + const resp = methodNotAllowedResponse(req, ['POST', 'OPTIONS']); + const body = await resp.text(); + return new Response(body, { + status: resp.status, + headers: { ...Object.fromEntries(resp.headers.entries()), ...baseHeaders } + }); + } - router.post('/', async (req, res) => { - res.setHeader('Cache-Control', 'no-store'); + if (limiter) { + const key = `${getClientAddress(req, ctx) ?? 'global'}:revoke`; + const rl = limiter.consume(key); + if (!rl.allowed) { + return jsonResponse( + new TooManyRequestsError('You have exceeded the rate limit for token revocation requests').toResponseObject(), + { + status: 429, + headers: { + ...baseHeaders, + ...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {}) + } + } + ); + } + } try { - const parseResult = OAuthTokenRevocationRequestSchema.safeParse(req.body); + const rawBody = await getParsedBody(req, ctx); + const parseResult = OAuthTokenRevocationRequestSchema.safeParse(rawBody); if (!parseResult.success) { throw new InvalidRequestError(parseResult.error.message); } - const client = req.client; - if (!client) { - // This should never happen - throw new ServerError('Internal Server Error'); - } + const client = await authenticateClient(rawBody, { clientsStore: provider.clientsStore }); await provider.revokeToken!(client, parseResult.data); - res.status(200).json({}); + return jsonResponse({}, { status: 200, headers: baseHeaders }); } catch (error) { if (error instanceof OAuthError) { const status = error instanceof ServerError ? 500 : 400; - res.status(status).json(error.toResponseObject()); - } else { - const serverError = new ServerError('Internal Server Error'); - res.status(500).json(serverError.toResponseObject()); + return jsonResponse(error.toResponseObject(), { status, headers: baseHeaders }); } + const serverError = new ServerError('Internal Server Error'); + return jsonResponse(serverError.toResponseObject(), { status: 500, headers: baseHeaders }); } - }); - - return router; + }; } diff --git a/packages/server/src/server/auth/handlers/token.ts b/packages/server/src/server/auth/handlers/token.ts index 3b7941294..096a10ff3 100644 --- a/packages/server/src/server/auth/handlers/token.ts +++ b/packages/server/src/server/auth/handlers/token.ts @@ -6,17 +6,22 @@ import { TooManyRequestsError, UnsupportedGrantTypeError } from '@modelcontextprotocol/core'; -import cors from 'cors'; -import type { RequestHandler } from 'express'; -import express from 'express'; -import type { Options as RateLimitOptions } from 'express-rate-limit'; -import { rateLimit } from 'express-rate-limit'; import { verifyChallenge } from 'pkce-challenge'; import * as z from 'zod/v4'; -import { allowedMethods } from '../middleware/allowedMethods.js'; import { authenticateClient } from '../middleware/clientAuth.js'; import type { OAuthServerProvider } from '../provider.js'; +import type { WebHandler } from '../web.js'; +import { + corsHeaders, + corsPreflightResponse, + getClientAddress, + getParsedBody, + InMemoryRateLimiter, + jsonResponse, + methodNotAllowedResponse, + noStoreHeaders +} from '../web.js'; export type TokenHandlerOptions = { provider: OAuthServerProvider; @@ -24,7 +29,7 @@ export type TokenHandlerOptions = { * Rate limiting configuration for the token endpoint. * Set to false to disable rate limiting for this endpoint. */ - rateLimit?: Partial | false; + rateLimit?: Partial<{ windowMs: number; max: number }> | false; }; const TokenRequestSchema = z.object({ @@ -44,53 +49,65 @@ const RefreshTokenGrantSchema = z.object({ resource: z.string().url().optional() }); -export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHandlerOptions): RequestHandler { - // Nested router so we can configure middleware and restrict HTTP method - const router = express.Router(); - - // Configure CORS to allow any origin, to make accessible to web-based MCP clients - router.use(cors()); - - router.use(allowedMethods(['POST'])); - router.use(express.urlencoded({ extended: false })); - - // Apply rate limiting unless explicitly disabled - if (rateLimitConfig !== false) { - router.use( - rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 50, // 50 requests per windowMs - standardHeaders: true, - legacyHeaders: false, - message: new TooManyRequestsError('You have exceeded the rate limit for token requests').toResponseObject(), - ...rateLimitConfig - }) - ); - } - - // Authenticate and extract client details - router.use(authenticateClient({ clientsStore: provider.clientsStore })); +export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHandlerOptions): WebHandler { + const limiter = + rateLimitConfig === false + ? undefined + : new InMemoryRateLimiter({ + windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1000, + max: rateLimitConfig?.max ?? 50 + }); + + const cors = { + allowOrigin: '*', + allowMethods: ['POST', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization'], + maxAgeSeconds: 60 * 60 * 24 + } as const; + + return async (req, ctx) => { + const baseHeaders = { ...corsHeaders(cors), ...noStoreHeaders() }; + + if (req.method === 'OPTIONS') { + return corsPreflightResponse(cors); + } + if (req.method !== 'POST') { + const resp = methodNotAllowedResponse(req, ['POST', 'OPTIONS']); + const body = await resp.text(); + return new Response(body, { + status: resp.status, + headers: { ...Object.fromEntries(resp.headers.entries()), ...baseHeaders } + }); + } - router.post('/', async (req, res) => { - res.setHeader('Cache-Control', 'no-store'); + if (limiter) { + const key = `${getClientAddress(req, ctx) ?? 'global'}:token`; + const rl = limiter.consume(key); + if (!rl.allowed) { + return jsonResponse(new TooManyRequestsError('You have exceeded the rate limit for token requests').toResponseObject(), { + status: 429, + headers: { + ...baseHeaders, + ...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {}) + } + }); + } + } try { - const parseResult = TokenRequestSchema.safeParse(req.body); + const rawBody = await getParsedBody(req, ctx); + const parseResult = TokenRequestSchema.safeParse(rawBody); if (!parseResult.success) { throw new InvalidRequestError(parseResult.error.message); } const { grant_type } = parseResult.data; - const client = req.client; - if (!client) { - // This should never happen - throw new ServerError('Internal Server Error'); - } + const client = await authenticateClient(rawBody, { clientsStore: provider.clientsStore }); switch (grant_type) { case 'authorization_code': { - const parseResult = AuthorizationCodeGrantSchema.safeParse(req.body); + const parseResult = AuthorizationCodeGrantSchema.safeParse(rawBody); if (!parseResult.success) { throw new InvalidRequestError(parseResult.error.message); } @@ -116,12 +133,11 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand redirect_uri, resource ? new URL(resource) : undefined ); - res.status(200).json(tokens); - break; + return jsonResponse(tokens, { status: 200, headers: baseHeaders }); } case 'refresh_token': { - const parseResult = RefreshTokenGrantSchema.safeParse(req.body); + const parseResult = RefreshTokenGrantSchema.safeParse(rawBody); if (!parseResult.success) { throw new InvalidRequestError(parseResult.error.message); } @@ -135,8 +151,7 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand scopes, resource ? new URL(resource) : undefined ); - res.status(200).json(tokens); - break; + return jsonResponse(tokens, { status: 200, headers: baseHeaders }); } // Additional auth methods will not be added on the server side of the SDK. case 'client_credentials': @@ -146,13 +161,10 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand } catch (error) { if (error instanceof OAuthError) { const status = error instanceof ServerError ? 500 : 400; - res.status(status).json(error.toResponseObject()); - } else { - const serverError = new ServerError('Internal Server Error'); - res.status(500).json(serverError.toResponseObject()); + return jsonResponse(error.toResponseObject(), { status, headers: baseHeaders }); } + const serverError = new ServerError('Internal Server Error'); + return jsonResponse(serverError.toResponseObject(), { status: 500, headers: baseHeaders }); } - }); - - return router; + }; } diff --git a/packages/server/src/server/auth/index.ts b/packages/server/src/server/auth/index.ts index 5369224cf..2b176805b 100644 --- a/packages/server/src/server/auth/index.ts +++ b/packages/server/src/server/auth/index.ts @@ -10,3 +10,4 @@ export * from './middleware/clientAuth.js'; export * from './provider.js'; export * from './providers/proxyProvider.js'; export * from './router.js'; +export * from './web.js'; diff --git a/packages/server/src/server/auth/middleware/allowedMethods.ts b/packages/server/src/server/auth/middleware/allowedMethods.ts index 72c076ec4..5c5245690 100644 --- a/packages/server/src/server/auth/middleware/allowedMethods.ts +++ b/packages/server/src/server/auth/middleware/allowedMethods.ts @@ -1,20 +1,20 @@ import { MethodNotAllowedError } from '@modelcontextprotocol/core'; -import type { RequestHandler } from 'express'; + +import { jsonResponse } from '../web.js'; /** - * Middleware to handle unsupported HTTP methods with a 405 Method Not Allowed response. + * Helper to handle unsupported HTTP methods with a 405 Method Not Allowed response. * * @param allowedMethods Array of allowed HTTP methods for this endpoint (e.g., ['GET', 'POST']) - * @returns Express middleware that returns a 405 error if method not in allowed list + * @returns Response if method not in allowed list, otherwise undefined */ -export function allowedMethods(allowedMethods: string[]): RequestHandler { - return (req, res, next) => { - if (allowedMethods.includes(req.method)) { - next(); - return; - } - - const error = new MethodNotAllowedError(`The method ${req.method} is not allowed for this endpoint`); - res.status(405).set('Allow', allowedMethods.join(', ')).json(error.toResponseObject()); - }; +export function allowedMethods(allowedMethods: string[], req: Request): Response | undefined { + if (allowedMethods.includes(req.method)) { + return undefined; + } + const error = new MethodNotAllowedError(`The method ${req.method} is not allowed for this endpoint`); + return jsonResponse(error.toResponseObject(), { + status: 405, + headers: { Allow: allowedMethods.join(', ') } + }); } diff --git a/packages/server/src/server/auth/middleware/bearerAuth.ts b/packages/server/src/server/auth/middleware/bearerAuth.ts index 1a16de1a9..853e400f6 100644 --- a/packages/server/src/server/auth/middleware/bearerAuth.ts +++ b/packages/server/src/server/auth/middleware/bearerAuth.ts @@ -1,8 +1,8 @@ import type { AuthInfo } from '@modelcontextprotocol/core'; import { InsufficientScopeError, InvalidTokenError, OAuthError, ServerError } from '@modelcontextprotocol/core'; -import type { RequestHandler } from 'express'; import type { OAuthTokenVerifier } from '../provider.js'; +import { jsonResponse } from '../web.js'; export type BearerAuthMiddlewareOptions = { /** @@ -21,83 +21,81 @@ export type BearerAuthMiddlewareOptions = { resourceMetadataUrl?: string; }; -declare module 'express-serve-static-core' { - interface Request { - /** - * Information about the validated access token, if the `requireBearerAuth` middleware was used. - */ - auth?: AuthInfo; - } -} - /** - * Middleware that requires a valid Bearer token in the Authorization header. + * Validates a Bearer token in the Authorization header. * - * This will validate the token with the auth provider and add the resulting auth info to the request object. - * - * If resourceMetadataUrl is provided, it will be included in the WWW-Authenticate header - * for 401 responses as per the OAuth 2.0 Protected Resource Metadata spec. + * Returns either `{ authInfo }` on success or `{ response }` on failure. */ -export function requireBearerAuth({ verifier, requiredScopes = [], resourceMetadataUrl }: BearerAuthMiddlewareOptions): RequestHandler { - return async (req, res, next) => { - try { - const authHeader = req.headers.authorization; - if (!authHeader) { - throw new InvalidTokenError('Missing Authorization header'); - } +export async function requireBearerAuth( + req: Request, + { verifier, requiredScopes = [], resourceMetadataUrl }: BearerAuthMiddlewareOptions +): Promise<{ authInfo: AuthInfo } | { response: Response }> { + try { + const authHeader = req.headers.get('authorization'); + if (!authHeader) { + throw new InvalidTokenError('Missing Authorization header'); + } - const [type, token] = authHeader.split(' '); - if (type!.toLowerCase() !== 'bearer' || !token) { - throw new InvalidTokenError("Invalid Authorization header format, expected 'Bearer TOKEN'"); - } + const [type, token] = authHeader.split(' '); + if (type!.toLowerCase() !== 'bearer' || !token) { + throw new InvalidTokenError("Invalid Authorization header format, expected 'Bearer TOKEN'"); + } - const authInfo = await verifier.verifyAccessToken(token); + const authInfo = await verifier.verifyAccessToken(token); - // Check if token has the required scopes (if any) - if (requiredScopes.length > 0) { - const hasAllScopes = requiredScopes.every(scope => authInfo.scopes.includes(scope)); + // Check if token has the required scopes (if any) + if (requiredScopes.length > 0) { + const hasAllScopes = requiredScopes.every(scope => authInfo.scopes.includes(scope)); - if (!hasAllScopes) { - throw new InsufficientScopeError('Insufficient scope'); - } + if (!hasAllScopes) { + throw new InsufficientScopeError('Insufficient scope'); } + } - // Check if the token is set to expire or if it is expired - if (typeof authInfo.expiresAt !== 'number' || isNaN(authInfo.expiresAt)) { - throw new InvalidTokenError('Token has no expiration time'); - } else if (authInfo.expiresAt < Date.now() / 1000) { - throw new InvalidTokenError('Token has expired'); + // Check if the token is set to expire or if it is expired + if (typeof authInfo.expiresAt !== 'number' || isNaN(authInfo.expiresAt)) { + throw new InvalidTokenError('Token has no expiration time'); + } else if (authInfo.expiresAt < Date.now() / 1000) { + throw new InvalidTokenError('Token has expired'); + } + + return { authInfo }; + } catch (error) { + // Build WWW-Authenticate header parts + const buildWwwAuthHeader = (errorCode: string, message: string): string => { + let header = `Bearer error="${errorCode}", error_description="${message}"`; + if (requiredScopes.length > 0) { + header += `, scope="${requiredScopes.join(' ')}"`; + } + if (resourceMetadataUrl) { + header += `, resource_metadata="${resourceMetadataUrl}"`; } + return header; + }; - req.auth = authInfo; - next(); - } catch (error) { - // Build WWW-Authenticate header parts - const buildWwwAuthHeader = (errorCode: string, message: string): string => { - let header = `Bearer error="${errorCode}", error_description="${message}"`; - if (requiredScopes.length > 0) { - header += `, scope="${requiredScopes.join(' ')}"`; - } - if (resourceMetadataUrl) { - header += `, resource_metadata="${resourceMetadataUrl}"`; - } - return header; + if (error instanceof InvalidTokenError) { + return { + response: jsonResponse(error.toResponseObject(), { + status: 401, + headers: { 'WWW-Authenticate': buildWwwAuthHeader(error.errorCode, error.message) } + }) }; - - if (error instanceof InvalidTokenError) { - res.set('WWW-Authenticate', buildWwwAuthHeader(error.errorCode, error.message)); - res.status(401).json(error.toResponseObject()); - } else if (error instanceof InsufficientScopeError) { - res.set('WWW-Authenticate', buildWwwAuthHeader(error.errorCode, error.message)); - res.status(403).json(error.toResponseObject()); - } else if (error instanceof ServerError) { - res.status(500).json(error.toResponseObject()); - } else if (error instanceof OAuthError) { - res.status(400).json(error.toResponseObject()); - } else { - const serverError = new ServerError('Internal Server Error'); - res.status(500).json(serverError.toResponseObject()); - } } - }; + if (error instanceof InsufficientScopeError) { + return { + response: jsonResponse(error.toResponseObject(), { + status: 403, + headers: { 'WWW-Authenticate': buildWwwAuthHeader(error.errorCode, error.message) } + }) + }; + } + if (error instanceof ServerError) { + return { response: jsonResponse(error.toResponseObject(), { status: 500 }) }; + } + if (error instanceof OAuthError) { + return { response: jsonResponse(error.toResponseObject(), { status: 400 }) }; + } + const serverError = new ServerError('Internal Server Error'); + return { response: jsonResponse(serverError.toResponseObject(), { status: 500 }) }; + } } diff --git a/packages/server/src/server/auth/middleware/clientAuth.ts b/packages/server/src/server/auth/middleware/clientAuth.ts index ac4bc8b79..9da271e35 100644 --- a/packages/server/src/server/auth/middleware/clientAuth.ts +++ b/packages/server/src/server/auth/middleware/clientAuth.ts @@ -1,6 +1,5 @@ import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import { InvalidClientError, InvalidRequestError, OAuthError, ServerError } from '@modelcontextprotocol/core'; -import type { RequestHandler } from 'express'; +import { InvalidClientError, InvalidRequestError } from '@modelcontextprotocol/core'; import * as z from 'zod/v4'; import type { OAuthRegisteredClientsStore } from '../clients.js'; @@ -17,49 +16,35 @@ const ClientAuthenticatedRequestSchema = z.object({ client_secret: z.string().optional() }); -declare module 'express-serve-static-core' { - interface Request { - /** - * The authenticated client for this request, if the `authenticateClient` middleware was used. - */ - client?: OAuthClientInformationFull; +/** + * Parses and validates client credentials from a request body, returning the authenticated client. + * + * Throws an OAuthError (or ServerError) on failure. + */ +export async function authenticateClient( + body: unknown, + { clientsStore }: ClientAuthenticationMiddlewareOptions +): Promise { + const result = ClientAuthenticatedRequestSchema.safeParse(body); + if (!result.success) { + throw new InvalidRequestError(String(result.error)); } -} - -export function authenticateClient({ clientsStore }: ClientAuthenticationMiddlewareOptions): RequestHandler { - return async (req, res, next) => { - try { - const result = ClientAuthenticatedRequestSchema.safeParse(req.body); - if (!result.success) { - throw new InvalidRequestError(String(result.error)); - } - const { client_id, client_secret } = result.data; - const client = await clientsStore.getClient(client_id); - if (!client) { - throw new InvalidClientError('Invalid client_id'); - } - if (client.client_secret) { - if (!client_secret) { - throw new InvalidClientError('Client secret is required'); - } - if (client.client_secret !== client_secret) { - throw new InvalidClientError('Invalid client_secret'); - } - if (client.client_secret_expires_at && client.client_secret_expires_at < Math.floor(Date.now() / 1000)) { - throw new InvalidClientError('Client secret has expired'); - } - } - - req.client = client; - next(); - } catch (error) { - if (error instanceof OAuthError) { - const status = error instanceof ServerError ? 500 : 400; - res.status(status).json(error.toResponseObject()); - } else { - const serverError = new ServerError('Internal Server Error'); - res.status(500).json(serverError.toResponseObject()); - } + const { client_id, client_secret } = result.data; + const client = await clientsStore.getClient(client_id); + if (!client) { + throw new InvalidClientError('Invalid client_id'); + } + if (client.client_secret) { + if (!client_secret) { + throw new InvalidClientError('Client secret is required'); } - }; + if (client.client_secret !== client_secret) { + throw new InvalidClientError('Invalid client_secret'); + } + if (client.client_secret_expires_at && client.client_secret_expires_at < Math.floor(Date.now() / 1000)) { + throw new InvalidClientError('Client secret has expired'); + } + } + + return client; } diff --git a/packages/server/src/server/auth/provider.ts b/packages/server/src/server/auth/provider.ts index 6d27fb792..d7dc395d1 100644 --- a/packages/server/src/server/auth/provider.ts +++ b/packages/server/src/server/auth/provider.ts @@ -1,5 +1,4 @@ import type { AuthInfo, OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; -import type { Response } from 'express'; import type { OAuthRegisteredClientsStore } from './clients.js'; @@ -27,7 +26,7 @@ export interface OAuthServerProvider { * - In the successful case, the redirect MUST include the `code` and `state` (if present) query parameters. * - In the error case, the redirect MUST include the `error` query parameter, and MAY include an optional `error_description` query parameter. */ - authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise; + authorize(client: OAuthClientInformationFull, params: AuthorizationParams): Promise; /** * Returns the `codeChallenge` that was used when the indicated authorization began. diff --git a/packages/server/src/server/auth/providers/proxyProvider.ts b/packages/server/src/server/auth/providers/proxyProvider.ts index 0688754c0..230e8766e 100644 --- a/packages/server/src/server/auth/providers/proxyProvider.ts +++ b/packages/server/src/server/auth/providers/proxyProvider.ts @@ -1,6 +1,5 @@ import type { AuthInfo, FetchLike, OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; import { OAuthClientInformationFullSchema, OAuthTokensSchema, ServerError } from '@modelcontextprotocol/core'; -import type { Response } from 'express'; import type { OAuthRegisteredClientsStore } from '../clients.js'; import type { AuthorizationParams, OAuthServerProvider } from '../provider.js'; @@ -112,7 +111,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider { }; } - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise { + async authorize(client: OAuthClientInformationFull, params: AuthorizationParams): Promise { // Start with required OAuth parameters const targetUrl = new URL(this._endpoints.authorizationUrl); const searchParams = new URLSearchParams({ @@ -129,7 +128,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider { if (params.resource) searchParams.set('resource', params.resource.href); targetUrl.search = searchParams.toString(); - res.redirect(targetUrl.toString()); + return Response.redirect(targetUrl.toString(), 302); } async challengeForAuthorizationCode(_client: OAuthClientInformationFull, _authorizationCode: string): Promise { diff --git a/packages/server/src/server/auth/router.ts b/packages/server/src/server/auth/router.ts index ba8b030e0..083657250 100644 --- a/packages/server/src/server/auth/router.ts +++ b/packages/server/src/server/auth/router.ts @@ -1,6 +1,4 @@ import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/core'; -import type { RequestHandler } from 'express'; -import express from 'express'; import type { AuthorizationHandlerOptions } from './handlers/authorize.js'; import { authorizationHandler } from './handlers/authorize.js'; @@ -12,6 +10,7 @@ import { revocationHandler } from './handlers/revoke.js'; import type { TokenHandlerOptions } from './handlers/token.js'; import { tokenHandler } from './handlers/token.js'; import type { OAuthServerProvider } from './provider.js'; +import type { WebHandler } from './web.js'; // Check for dev mode flag that allows HTTP issuer URLs (for development/testing only) const allowInsecureIssuerUrl = @@ -67,6 +66,24 @@ export type AuthRouterOptions = { tokenOptions?: Omit; }; +export type AuthRoute = { + path: string; + methods: string[]; + handler: WebHandler; +}; + +export type WebAuthRouter = { + /** + * List of concrete routes (absolute paths) that should be mounted at the application root. + */ + routes: AuthRoute[]; + + /** + * Convenience dispatcher that matches on `new URL(req.url).pathname` and calls the correct handler. + */ + handle: WebHandler; +}; + const checkIssuerUrl = (issuer: URL): void => { // Technically RFC 8414 does not permit a localhost HTTPS exemption, but this will be necessary for ease of testing if (issuer.protocol !== 'https:' && issuer.hostname !== 'localhost' && issuer.hostname !== '127.0.0.1' && !allowInsecureIssuerUrl) { @@ -126,53 +143,62 @@ export const createOAuthMetadata = (options: { * Note: if your MCP server is only a resource server and not an authorization server, use mcpAuthMetadataRouter instead. * * By default, rate limiting is applied to all endpoints to prevent abuse. - * - * This router MUST be installed at the application root, like so: - * - * const app = express(); - * app.use(mcpAuthRouter(...)); */ -export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler { +export function mcpAuthRouter(options: AuthRouterOptions): WebAuthRouter { const oauthMetadata = createOAuthMetadata(options); - - const router = express.Router(); - - router.use( - new URL(oauthMetadata.authorization_endpoint).pathname, - authorizationHandler({ provider: options.provider, ...options.authorizationOptions }) - ); - - router.use(new URL(oauthMetadata.token_endpoint).pathname, tokenHandler({ provider: options.provider, ...options.tokenOptions })); - - router.use( - mcpAuthMetadataRouter({ - oauthMetadata, - // Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat) - resourceServerUrl: options.resourceServerUrl ?? options.baseUrl ?? new URL(oauthMetadata.issuer), - serviceDocumentationUrl: options.serviceDocumentationUrl, - scopesSupported: options.scopesSupported, - resourceName: options.resourceName - }) - ); + const routes: AuthRoute[] = []; + + routes.push({ + path: new URL(oauthMetadata.authorization_endpoint).pathname, + methods: ['GET', 'POST'], + handler: authorizationHandler({ provider: options.provider, ...options.authorizationOptions }) + }); + + routes.push({ + path: new URL(oauthMetadata.token_endpoint).pathname, + methods: ['POST', 'OPTIONS'], + handler: tokenHandler({ provider: options.provider, ...options.tokenOptions }) + }); + + const metadataRouter = mcpAuthMetadataRouter({ + oauthMetadata, + // Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat) + resourceServerUrl: options.resourceServerUrl ?? options.baseUrl ?? new URL(oauthMetadata.issuer), + serviceDocumentationUrl: options.serviceDocumentationUrl, + scopesSupported: options.scopesSupported, + resourceName: options.resourceName + }); + routes.push(...metadataRouter.routes); if (oauthMetadata.registration_endpoint) { - router.use( - new URL(oauthMetadata.registration_endpoint).pathname, - clientRegistrationHandler({ + routes.push({ + path: new URL(oauthMetadata.registration_endpoint).pathname, + methods: ['POST', 'OPTIONS'], + handler: clientRegistrationHandler({ clientsStore: options.provider.clientsStore, ...options.clientRegistrationOptions }) - ); + }); } if (oauthMetadata.revocation_endpoint) { - router.use( - new URL(oauthMetadata.revocation_endpoint).pathname, - revocationHandler({ provider: options.provider, ...options.revocationOptions }) - ); + routes.push({ + path: new URL(oauthMetadata.revocation_endpoint).pathname, + methods: ['POST', 'OPTIONS'], + handler: revocationHandler({ provider: options.provider, ...options.revocationOptions }) + }); } - return router; + const handle: WebHandler = async (req, ctx) => { + const pathname = new URL(req.url).pathname; + const route = routes.find(r => r.path === pathname); + if (!route) { + return new Response('Not Found', { status: 404 }); + } + return route.handler(req, ctx); + }; + + return { routes, handle }; } export type AuthMetadataOptions = { @@ -203,11 +229,9 @@ export type AuthMetadataOptions = { resourceName?: string; }; -export function mcpAuthMetadataRouter(options: AuthMetadataOptions): express.Router { +export function mcpAuthMetadataRouter(options: AuthMetadataOptions): WebAuthRouter { checkIssuerUrl(new URL(options.oauthMetadata.issuer)); - const router = express.Router(); - const protectedResourceMetadata: OAuthProtectedResourceMetadata = { resource: options.resourceServerUrl.href, @@ -220,12 +244,24 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions): express.Rou // Serve PRM at the path-specific URL per RFC 9728 const rsPath = new URL(options.resourceServerUrl.href).pathname; - router.use(`/.well-known/oauth-protected-resource${rsPath === '/' ? '' : rsPath}`, metadataHandler(protectedResourceMetadata)); - - // Always add this for OAuth Authorization Server metadata per RFC 8414 - router.use('/.well-known/oauth-authorization-server', metadataHandler(options.oauthMetadata)); + const prmPath = `/.well-known/oauth-protected-resource${rsPath === '/' ? '' : rsPath}`; + + const routes: AuthRoute[] = [ + { path: prmPath, methods: ['GET', 'OPTIONS'], handler: metadataHandler(protectedResourceMetadata) }, + // Always add this for OAuth Authorization Server metadata per RFC 8414 + { path: '/.well-known/oauth-authorization-server', methods: ['GET', 'OPTIONS'], handler: metadataHandler(options.oauthMetadata) } + ]; + + const handle: WebHandler = async (req, ctx) => { + const pathname = new URL(req.url).pathname; + const route = routes.find(r => r.path === pathname); + if (!route) { + return new Response('Not Found', { status: 404 }); + } + return route.handler(req, ctx); + }; - return router; + return { routes, handle }; } /** diff --git a/packages/server/src/server/auth/web.ts b/packages/server/src/server/auth/web.ts new file mode 100644 index 000000000..a16ada4ef --- /dev/null +++ b/packages/server/src/server/auth/web.ts @@ -0,0 +1,140 @@ +import { MethodNotAllowedError } from '@modelcontextprotocol/core'; + +export type HeaderMap = Record; + +export type WebHandlerContext = { + /** + * Optional pre-parsed request body from an upstream framework. + * If provided, handlers will use this instead of reading from the Request stream. + */ + parsedBody?: unknown; + + /** + * Optional client address for rate limiting (e.g., IP). + */ + clientAddress?: string; +}; + +export type WebHandler = (req: Request, ctx?: WebHandlerContext) => Promise; + +export function jsonResponse(body: unknown, init?: { status?: number; headers?: HeaderMap }): Response { + const headers: HeaderMap = { 'Content-Type': 'application/json' }; + if (init?.headers) { + Object.assign(headers, init.headers); + } + return new Response(JSON.stringify(body), { + status: init?.status ?? 200, + headers + }); +} + +export function noStoreHeaders(): HeaderMap { + return { 'Cache-Control': 'no-store' }; +} + +export function getClientAddress(req: Request, ctx?: WebHandlerContext): string | undefined { + if (ctx?.clientAddress) return ctx.clientAddress; + const xff = req.headers.get('x-forwarded-for'); + if (xff) return xff.split(',')[0]?.trim(); + return undefined; +} + +export async function getParsedBody(req: Request, ctx?: WebHandlerContext): Promise { + if (ctx?.parsedBody !== undefined) { + return ctx.parsedBody; + } + + const ct = req.headers.get('content-type') ?? ''; + + if (ct.includes('application/json')) { + return await req.json(); + } + + if (ct.includes('application/x-www-form-urlencoded')) { + const text = await req.text(); + return objectFromUrlEncoded(text); + } + + // Empty bodies are treated as empty objects. + const text = await req.text(); + if (!text) return {}; + + // If content-type is missing/unknown, fall back to treating it as urlencoded-like. + return objectFromUrlEncoded(text); +} + +export function objectFromUrlEncoded(body: string): Record { + const params = new URLSearchParams(body); + const out: Record = {}; + for (const [k, v] of params.entries()) out[k] = v; + return out; +} + +export function methodNotAllowedResponse(req: Request, allowed: string[]): Response { + const error = new MethodNotAllowedError(`The method ${req.method} is not allowed for this endpoint`); + return jsonResponse(error.toResponseObject(), { + status: 405, + headers: { Allow: allowed.join(', ') } + }); +} + +export type CorsOptions = { + allowOrigin?: string; + allowMethods: readonly string[]; + allowHeaders?: readonly string[]; + exposeHeaders?: readonly string[]; + maxAgeSeconds?: number; +}; + +export function corsHeaders(options: CorsOptions): HeaderMap { + return { + 'Access-Control-Allow-Origin': options.allowOrigin ?? '*', + 'Access-Control-Allow-Methods': options.allowMethods.join(', '), + 'Access-Control-Allow-Headers': (options.allowHeaders ?? ['Content-Type', 'Authorization']).join(', '), + ...(options.exposeHeaders ? { 'Access-Control-Expose-Headers': options.exposeHeaders.join(', ') } : {}), + ...(options.maxAgeSeconds !== undefined ? { 'Access-Control-Max-Age': String(options.maxAgeSeconds) } : {}) + }; +} + +export function corsPreflightResponse(options: CorsOptions): Response { + return new Response(null, { + status: 204, + headers: corsHeaders(options) + }); +} + +export type InMemoryRateLimitConfig = { + windowMs: number; + max: number; +}; + +type RateState = { windowStart: number; count: number }; + +/** + * Minimal in-memory rate limiter for single-process deployments. + * Not suitable for distributed setups without an external store. + */ +export class InMemoryRateLimiter { + private _state = new Map(); + + constructor(private _config: InMemoryRateLimitConfig) {} + + consume(key: string): { allowed: boolean; retryAfterSeconds?: number } { + const now = Date.now(); + const windowStart = now - (now % this._config.windowMs); + const existing = this._state.get(key); + + if (!existing || existing.windowStart !== windowStart) { + this._state.set(key, { windowStart, count: 1 }); + return { allowed: true }; + } + + if (existing.count >= this._config.max) { + const retryAfterMs = windowStart + this._config.windowMs - now; + return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1000)) }; + } + + existing.count += 1; + return { allowed: true }; + } +} diff --git a/packages/server/src/server/middleware/hostHeaderValidation.ts b/packages/server/src/server/middleware/hostHeaderValidation.ts index f46178db3..e4d13ecf5 100644 --- a/packages/server/src/server/middleware/hostHeaderValidation.ts +++ b/packages/server/src/server/middleware/hostHeaderValidation.ts @@ -1,79 +1,67 @@ -import type { NextFunction, Request, RequestHandler, Response } from 'express'; +export type HostHeaderValidationResult = + | { ok: true; hostname: string } + | { + ok: false; + errorCode: 'missing_host' | 'invalid_host_header' | 'invalid_host'; + message: string; + hostHeader?: string; + hostname?: string; + }; /** - * Express middleware for DNS rebinding protection. - * Validates Host header hostname (port-agnostic) against an allowed list. + * Parse and validate a Host header against an allowlist of hostnames (port-agnostic). * - * This is particularly important for servers without authorization or HTTPS, - * such as localhost servers or development servers. DNS rebinding attacks can - * bypass same-origin policy by manipulating DNS to point a domain to a - * localhost address, allowing malicious websites to access your local server. - * - * @param allowedHostnames - List of allowed hostnames (without ports). - * For IPv6, provide the address with brackets (e.g., '[::1]'). - * @returns Express middleware function - * - * @example - * ```typescript - * const middleware = hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); - * app.use(middleware); - * ``` + * - Input host header may include a port (e.g. `localhost:3000`) or IPv6 brackets (e.g. `[::1]:3000`). + * - Allowlist items should be hostnames only (no ports). For IPv6, include brackets (e.g. `[::1]`). */ -export function hostHeaderValidation(allowedHostnames: string[]): RequestHandler { - return (req: Request, res: Response, next: NextFunction) => { - const hostHeader = req.headers.host; - if (!hostHeader) { - res.status(403).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Missing Host header' - }, - id: null - }); - return; - } +export function validateHostHeader(hostHeader: string | null | undefined, allowedHostnames: string[]): HostHeaderValidationResult { + if (!hostHeader) { + return { ok: false, errorCode: 'missing_host', message: 'Missing Host header' }; + } - // Use URL API to parse hostname (handles IPv4, IPv6, and regular hostnames) - let hostname: string; - try { - hostname = new URL(`http://${hostHeader}`).hostname; - } catch { - res.status(403).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: `Invalid Host header: ${hostHeader}` - }, - id: null - }); - return; - } + // Use URL API to parse hostname (handles IPv4, IPv6, and regular hostnames) + let hostname: string; + try { + hostname = new URL(`http://${hostHeader}`).hostname; + } catch { + return { ok: false, errorCode: 'invalid_host_header', message: `Invalid Host header: ${hostHeader}`, hostHeader }; + } - if (!allowedHostnames.includes(hostname)) { - res.status(403).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: `Invalid Host: ${hostname}` - }, - id: null - }); - return; - } - next(); - }; + if (!allowedHostnames.includes(hostname)) { + return { ok: false, errorCode: 'invalid_host', message: `Invalid Host: ${hostname}`, hostHeader, hostname }; + } + + return { ok: true, hostname }; } /** - * Convenience middleware for localhost DNS rebinding protection. - * Allows only localhost, 127.0.0.1, and [::1] (IPv6 localhost) hostnames. - * + * Convenience allowlist for localhost DNS rebinding protection. + */ +export function localhostAllowedHostnames(): string[] { + return ['localhost', '127.0.0.1', '[::1]']; +} + +/** + * Web-standard Request helper for DNS rebinding protection. * @example - * ```typescript - * app.use(localhostHostValidation()); - * ``` + * const result = validateHostHeader(req.headers.get('host'), ['localhost']) */ -export function localhostHostValidation(): RequestHandler { - return hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); +export function hostHeaderValidationResponse(req: Request, allowedHostnames: string[]): Response | undefined { + const result = validateHostHeader(req.headers.get('host'), allowedHostnames); + if (result.ok) return undefined; + + return new Response( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: result.message + }, + id: null + }), + { + status: 403, + headers: { 'Content-Type': 'application/json' } + } + ); } diff --git a/packages/server/src/server/sse.ts b/packages/server/src/server/sse.ts index 4fd0fa1d6..44117d0dd 100644 --- a/packages/server/src/server/sse.ts +++ b/packages/server/src/server/sse.ts @@ -16,24 +16,24 @@ export interface SSEServerTransportOptions { /** * List of allowed host header values for DNS rebinding protection. * If not specified, host validation is disabled. - * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. + * @deprecated Use the `hostHeaderValidationResponse` helper from `@modelcontextprotocol/server`, + * or use `createMcpExpressApp` from `@modelcontextprotocol/server-express` which includes localhost protection by default. */ allowedHosts?: string[]; /** * List of allowed origin header values for DNS rebinding protection. * If not specified, origin validation is disabled. - * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. + * @deprecated Use the `hostHeaderValidationResponse` helper from `@modelcontextprotocol/server`, + * or use `createMcpExpressApp` from `@modelcontextprotocol/server-express` which includes localhost protection by default. */ allowedOrigins?: string[]; /** * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). * Default is false for backwards compatibility. - * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. + * @deprecated Use the `hostHeaderValidationResponse` helper from `@modelcontextprotocol/server`, + * or use `createMcpExpressApp` from `@modelcontextprotocol/server-express` which includes localhost protection by default. */ enableDnsRebindingProtection?: boolean; } diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index f9ee07ca8..354f640f9 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -8,8 +8,9 @@ */ import type { IncomingMessage, ServerResponse } from 'node:http'; +import { Readable } from 'node:stream'; +import { URL } from 'node:url'; -import { getRequestListener } from '@hono/node-server'; import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport } from '@modelcontextprotocol/core'; import type { WebStandardStreamableHTTPServerTransportOptions } from './webStandardStreamableHttp.js'; @@ -22,12 +23,117 @@ import { WebStandardStreamableHTTPServerTransport } from './webStandardStreamabl */ export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions; +type NodeToWebRequestOptions = { + parsedBody?: unknown; +}; + +function getRequestUrl(req: IncomingMessage): URL { + const host = req.headers.host ?? 'localhost'; + const isTls = Boolean((req.socket as { encrypted?: boolean } | undefined)?.encrypted); + const protocol = isTls ? 'https' : 'http'; + const path = req.url ?? '/'; + return new URL(path, `${protocol}://${host}`); +} + +function toHeaders(req: IncomingMessage): Headers { + const headers = new Headers(); + for (const [key, value] of Object.entries(req.headers)) { + if (value === undefined) continue; + if (Array.isArray(value)) { + // Preserve multi-value headers as a comma-joined value. + // (Set-Cookie does not appear on requests; this is fine here.) + headers.set(key, value.join(', ')); + } else { + headers.set(key, value); + } + } + return headers; +} + +async function readBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks); +} + +async function nodeToWebRequest(req: IncomingMessage, options?: NodeToWebRequestOptions): Promise { + const url = getRequestUrl(req); + const method = req.method ?? 'GET'; + const headers = toHeaders(req); + + // If an upstream framework already parsed the body, the IncomingMessage stream + // may be consumed; rely on parsedBody instead of trying to read again. + if (options?.parsedBody !== undefined) { + return new Request(url, { method, headers }); + } + + // Only attach bodies for methods that can carry one. + if (method === 'GET' || method === 'HEAD') { + return new Request(url, { method, headers }); + } + + const body = await readBody(req); + return new Request(url, { method, headers, body }); +} + +function writeWebResponse(res: ServerResponse, webResponse: Response): Promise { + res.statusCode = webResponse.status; + + // Prefer undici's multi Set-Cookie support when available. + // Note: must call with the correct `this` (undici brand-checks Headers). + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getSetCookie = (webResponse.headers as any).getSetCookie as (() => string[]) | undefined; + const setCookies = typeof getSetCookie === 'function' ? getSetCookie.call(webResponse.headers) : undefined; + + for (const [key, value] of webResponse.headers.entries()) { + // We'll handle Set-Cookie separately if we have structured values. + if (key.toLowerCase() === 'set-cookie' && setCookies?.length) continue; + res.setHeader(key, value); + } + + if (setCookies?.length) { + res.setHeader('set-cookie', setCookies); + } + + // Node requires writing headers before streaming body. + res.flushHeaders?.(); + + if (!webResponse.body) { + res.end(); + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const readable = Readable.fromWeb(webResponse.body as unknown as ReadableStream); + readable.on('error', err => { + try { + res.destroy(err as Error); + } catch { + // ignore + } + reject(err); + }); + res.on('error', reject); + res.on('close', () => { + try { + readable.destroy(); + } catch { + // ignore + } + }); + readable.pipe(res); + res.on('finish', () => resolve()); + }); +} + /** * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. * It supports both SSE streaming and direct HTTP responses. * * This is a wrapper around `WebStandardStreamableHTTPServerTransport` that provides Node.js HTTP compatibility. - * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs. + * It converts between Node.js HTTP (IncomingMessage/ServerResponse) and Web Standard Request/Response. * * Usage example: * @@ -61,23 +167,9 @@ export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServ */ export class StreamableHTTPServerTransport implements Transport { private _webStandardTransport: WebStandardStreamableHTTPServerTransport; - private _requestListener: ReturnType; - // Store auth and parsedBody per request for passing through to handleRequest - private _requestContext: WeakMap = new WeakMap(); constructor(options: StreamableHTTPServerTransportOptions = {}) { this._webStandardTransport = new WebStandardStreamableHTTPServerTransport(options); - - // Create a request listener that wraps the web standard transport - // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming - this._requestListener = getRequestListener(async (webRequest: Request) => { - // Get context if available (set during handleRequest) - const context = this._requestContext.get(webRequest); - return this._webStandardTransport.handleRequest(webRequest, { - authInfo: context?.authInfo, - parsedBody: context?.parsedBody - }); - }); } /** @@ -153,21 +245,13 @@ export class StreamableHTTPServerTransport implements Transport { * @param parsedBody - Optional pre-parsed body from body-parser middleware */ async handleRequest(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise { - // Store context for this request to pass through auth and parsedBody - // We need to intercept the request creation to attach this context const authInfo = req.auth; - - // Create a custom handler that includes our context - const handler = getRequestListener(async (webRequest: Request) => { - return this._webStandardTransport.handleRequest(webRequest, { - authInfo, - parsedBody - }); + const webRequest = await nodeToWebRequest(req, { parsedBody }); + const webResponse = await this._webStandardTransport.handleRequest(webRequest, { + authInfo, + parsedBody }); - - // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion - // including proper SSE streaming support - await handler(req, res); + await writeWebResponse(res, webResponse); } /** diff --git a/packages/server/test/server/auth/handlers/authorize.test.ts b/packages/server/test/server/auth/handlers/authorize.test.ts index b84de3bc3..e5e65d72c 100644 --- a/packages/server/test/server/auth/handlers/authorize.test.ts +++ b/packages/server/test/server/auth/handlers/authorize.test.ts @@ -1,60 +1,41 @@ -import type { AuthInfo, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; -import { InvalidTokenError } from '@modelcontextprotocol/core'; -import type { Response } from 'express'; -import express from 'express'; -import supertest from 'supertest'; +import type { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { AuthorizationHandlerOptions } from '../../../../src/server/auth/handlers/authorize.js'; import { authorizationHandler } from '../../../../src/server/auth/handlers/authorize.js'; import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; -describe('Authorization Handler', () => { - // Mock client data +describe('authorizationHandler (web)', () => { const validClient: OAuthClientInformationFull = { client_id: 'valid-client', client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'], - scope: 'profile email' + redirect_uris: ['https://example.com/callback'] }; const multiRedirectClient: OAuthClientInformationFull = { client_id: 'multi-redirect-client', client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback1', 'https://example.com/callback2'], - scope: 'profile email' + redirect_uris: ['https://example.com/callback1', 'https://example.com/callback2'] }; - // Mock client store - const mockClientStore: OAuthRegisteredClientsStore = { - async getClient(clientId: string): Promise { - if (clientId === 'valid-client') { - return validClient; - } else if (clientId === 'multi-redirect-client') { - return multiRedirectClient; - } + const clientsStore: OAuthRegisteredClientsStore = { + async getClient(clientId: string) { + if (clientId === 'valid-client') return validClient; + if (clientId === 'multi-redirect-client') return multiRedirectClient; return undefined; } }; - // Mock provider - const mockProvider: OAuthServerProvider = { - clientsStore: mockClientStore, - - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise { - // Mock implementation - redirects to redirectUri with code and state - const redirectUrl = new URL(params.redirectUri); - redirectUrl.searchParams.set('code', 'mock_auth_code'); - if (params.state) { - redirectUrl.searchParams.set('state', params.state); - } - res.redirect(302, redirectUrl.toString()); + const provider: OAuthServerProvider = { + clientsStore, + async authorize(_client, params: AuthorizationParams): Promise { + const u = new URL(params.redirectUri); + u.searchParams.set('code', 'mock_auth_code'); + if (params.state) u.searchParams.set('state', params.state); + return Response.redirect(u.toString(), 302); }, - async challengeForAuthorizationCode(): Promise { return 'mock_challenge'; }, - async exchangeAuthorizationCode(): Promise { return { access_token: 'mock_access_token', @@ -63,7 +44,6 @@ describe('Authorization Handler', () => { refresh_token: 'mock_refresh_token' }; }, - async exchangeRefreshToken(): Promise { return { access_token: 'new_mock_access_token', @@ -72,225 +52,53 @@ describe('Authorization Handler', () => { refresh_token: 'new_mock_refresh_token' }; }, - - async verifyAccessToken(token: string): Promise { - if (token === 'valid_token') { - return { - token, - clientId: 'valid-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 - }; - } - throw new InvalidTokenError('Token is invalid or expired'); - }, - - async revokeToken(): Promise { - // Do nothing in mock + async verifyAccessToken() { + throw new Error('not used'); } }; - // Setup express app with handler - let app: express.Express; - let options: AuthorizationHandlerOptions; - - beforeEach(() => { - app = express(); - options = { provider: mockProvider }; - const handler = authorizationHandler(options); - app.use('/authorize', handler); + it('returns 405 for unsupported methods', async () => { + const handler = authorizationHandler({ provider }); + const res = await handler(new Request('http://localhost/authorize', { method: 'PUT' })); + expect(res.status).toBe(405); }); - describe('HTTP method validation', () => { - it('rejects non-GET/POST methods', async () => { - const response = await supertest(app).put('/authorize').query({ client_id: 'valid-client' }); - - expect(response.status).toBe(405); // Method not allowed response from handler - }); + it('returns 400 if client does not exist', async () => { + const handler = authorizationHandler({ provider }); + const res = await handler( + new Request( + 'http://localhost/authorize?client_id=missing&response_type=code&code_challenge=x&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback', + { method: 'GET' } + ) + ); + expect(res.status).toBe(400); + expect(await res.json()).toEqual(expect.objectContaining({ error: 'invalid_client' })); }); - describe('Client validation', () => { - it('requires client_id parameter', async () => { - const response = await supertest(app).get('/authorize'); - - expect(response.status).toBe(400); - expect(response.text).toContain('client_id'); - }); - - it('validates that client exists', async () => { - const response = await supertest(app).get('/authorize').query({ client_id: 'nonexistent-client' }); - - expect(response.status).toBe(400); - }); + it('redirects with a code on valid request (single redirect_uri inferred)', async () => { + const handler = authorizationHandler({ provider, rateLimit: false }); + const res = await handler( + new Request( + 'http://localhost/authorize?client_id=valid-client&response_type=code&code_challenge=challenge123&code_challenge_method=S256', + { method: 'GET' } + ) + ); + expect(res.status).toBe(302); + const location = res.headers.get('location')!; + expect(location).toContain('https://example.com/callback'); + expect(location).toContain('code=mock_auth_code'); + expect(res.headers.get('cache-control')).toBe('no-store'); }); - describe('Redirect URI validation', () => { - it('uses the only redirect_uri if client has just one and none provided', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256' - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.origin + location.pathname).toBe('https://example.com/callback'); - }); - - it('requires redirect_uri if client has multiple', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'multi-redirect-client', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256' - }); - - expect(response.status).toBe(400); - }); - - it('validates redirect_uri against client registered URIs', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - redirect_uri: 'https://malicious.com/callback', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256' - }); - - expect(response.status).toBe(400); - }); - - it('accepts valid redirect_uri that client registered with', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - redirect_uri: 'https://example.com/callback', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256' - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.origin + location.pathname).toBe('https://example.com/callback'); - }); - }); - - describe('Authorization request validation', () => { - it('requires response_type=code', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - redirect_uri: 'https://example.com/callback', - response_type: 'token', // invalid - we only support code flow - code_challenge: 'challenge123', - code_challenge_method: 'S256' - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.searchParams.get('error')).toBe('invalid_request'); - }); - - it('requires code_challenge parameter', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - redirect_uri: 'https://example.com/callback', - response_type: 'code', - code_challenge_method: 'S256' - // Missing code_challenge - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.searchParams.get('error')).toBe('invalid_request'); - }); - - it('requires code_challenge_method=S256', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - redirect_uri: 'https://example.com/callback', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'plain' // Only S256 is supported - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.searchParams.get('error')).toBe('invalid_request'); - }); - }); - - describe('Resource parameter validation', () => { - it('propagates resource parameter', async () => { - const mockProviderWithResource = vi.spyOn(mockProvider, 'authorize'); - - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - redirect_uri: 'https://example.com/callback', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256', - resource: 'https://api.example.com/resource' - }); - - expect(response.status).toBe(302); - expect(mockProviderWithResource).toHaveBeenCalledWith( - validClient, - expect.objectContaining({ - resource: new URL('https://api.example.com/resource'), - redirectUri: 'https://example.com/callback', - codeChallenge: 'challenge123' - }), - expect.any(Object) - ); - }); - }); - - describe('Successful authorization', () => { - it('handles successful authorization with all parameters', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - redirect_uri: 'https://example.com/callback', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256', - scope: 'profile email', - state: 'xyz789' - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.origin + location.pathname).toBe('https://example.com/callback'); - expect(location.searchParams.get('code')).toBe('mock_auth_code'); - expect(location.searchParams.get('state')).toBe('xyz789'); - }); - - it('preserves state parameter in response', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - redirect_uri: 'https://example.com/callback', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256', - state: 'state-value-123' - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.searchParams.get('state')).toBe('state-value-123'); - }); - - it('handles POST requests the same as GET', async () => { - const response = await supertest(app).post('/authorize').type('form').send({ - client_id: 'valid-client', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256' - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.searchParams.has('code')).toBe(true); - }); + it('requires redirect_uri if client has multiple redirect URIs', async () => { + const handler = authorizationHandler({ provider, rateLimit: false }); + const res = await handler( + new Request( + 'http://localhost/authorize?client_id=multi-redirect-client&response_type=code&code_challenge=challenge123&code_challenge_method=S256', + { method: 'GET' } + ) + ); + expect(res.status).toBe(400); + expect(await res.json()).toEqual(expect.objectContaining({ error: 'invalid_request' })); }); }); diff --git a/packages/server/test/server/auth/handlers/metadata.test.ts b/packages/server/test/server/auth/handlers/metadata.test.ts index 0dc51e51d..722320925 100644 --- a/packages/server/test/server/auth/handlers/metadata.test.ts +++ b/packages/server/test/server/auth/handlers/metadata.test.ts @@ -1,6 +1,4 @@ import type { OAuthMetadata } from '@modelcontextprotocol/core'; -import express from 'express'; -import supertest from 'supertest'; import { metadataHandler } from '../../../../src/server/auth/handlers/metadata.js'; @@ -18,62 +16,65 @@ describe('Metadata Handler', () => { code_challenge_methods_supported: ['S256'] }; - let app: express.Express; - - beforeEach(() => { - // Setup express app with metadata handler - app = express(); - app.use('/.well-known/oauth-authorization-server', metadataHandler(exampleMetadata)); - }); - it('requires GET method', async () => { - const response = await supertest(app).post('/.well-known/oauth-authorization-server').send({}); + const handler = metadataHandler(exampleMetadata); + const res = await handler(new Request('http://localhost/.well-known/oauth-authorization-server', { method: 'POST' })); - expect(response.status).toBe(405); - expect(response.headers.allow).toBe('GET, OPTIONS'); - expect(response.body).toEqual({ + expect(res.status).toBe(405); + expect(res.headers.get('allow')).toBe('GET, OPTIONS'); + expect(await res.json()).toEqual({ error: 'method_not_allowed', error_description: 'The method POST is not allowed for this endpoint' }); }); it('returns the metadata object', async () => { - const response = await supertest(app).get('/.well-known/oauth-authorization-server'); + const handler = metadataHandler(exampleMetadata); + const res = await handler(new Request('http://localhost/.well-known/oauth-authorization-server', { method: 'GET' })); - expect(response.status).toBe(200); - expect(response.body).toEqual(exampleMetadata); + expect(res.status).toBe(200); + expect(await res.json()).toEqual(exampleMetadata); }); it('includes CORS headers in response', async () => { - const response = await supertest(app).get('/.well-known/oauth-authorization-server').set('Origin', 'https://example.com'); + const handler = metadataHandler(exampleMetadata); + const res = await handler( + new Request('http://localhost/.well-known/oauth-authorization-server', { + method: 'GET', + headers: { Origin: 'https://example.com' } + }) + ); - expect(response.header['access-control-allow-origin']).toBe('*'); + expect(res.headers.get('access-control-allow-origin')).toBe('*'); }); it('supports OPTIONS preflight requests', async () => { - const response = await supertest(app) - .options('/.well-known/oauth-authorization-server') - .set('Origin', 'https://example.com') - .set('Access-Control-Request-Method', 'GET'); + const handler = metadataHandler(exampleMetadata); + const res = await handler( + new Request('http://localhost/.well-known/oauth-authorization-server', { + method: 'OPTIONS', + headers: { + Origin: 'https://example.com', + 'Access-Control-Request-Method': 'GET' + } + }) + ); - expect(response.status).toBe(204); - expect(response.header['access-control-allow-origin']).toBe('*'); + expect(res.status).toBe(204); + expect(res.headers.get('access-control-allow-origin')).toBe('*'); }); it('works with minimal metadata', async () => { - // Setup a new express app with minimal metadata - const minimalApp = express(); const minimalMetadata: OAuthMetadata = { issuer: 'https://auth.example.com', authorization_endpoint: 'https://auth.example.com/authorize', token_endpoint: 'https://auth.example.com/token', response_types_supported: ['code'] }; - minimalApp.use('/.well-known/oauth-authorization-server', metadataHandler(minimalMetadata)); - - const response = await supertest(minimalApp).get('/.well-known/oauth-authorization-server'); + const handler = metadataHandler(minimalMetadata); + const res = await handler(new Request('http://localhost/.well-known/oauth-authorization-server', { method: 'GET' })); - expect(response.status).toBe(200); - expect(response.body).toEqual(minimalMetadata); + expect(res.status).toBe(200); + expect(await res.json()).toEqual(minimalMetadata); }); }); diff --git a/packages/server/test/server/auth/handlers/register.test.ts b/packages/server/test/server/auth/handlers/register.test.ts index b10e048ed..1d3673c3f 100644 --- a/packages/server/test/server/auth/handlers/register.test.ts +++ b/packages/server/test/server/auth/handlers/register.test.ts @@ -1,274 +1,39 @@ -import type { OAuthClientInformationFull, OAuthClientMetadata } from '@modelcontextprotocol/core'; -import express from 'express'; -import supertest from 'supertest'; -import type { MockInstance } from 'vitest'; +import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { ClientRegistrationHandlerOptions } from '../../../../src/server/auth/handlers/register.js'; import { clientRegistrationHandler } from '../../../../src/server/auth/handlers/register.js'; -describe('Client Registration Handler', () => { - // Mock client store with registration support - const mockClientStoreWithRegistration: OAuthRegisteredClientsStore = { - async getClient(_clientId: string): Promise { - return undefined; - }, - - async registerClient(client: OAuthClientInformationFull): Promise { - // Return the client info as-is in the mock - return client; - } - }; - - // Mock client store without registration support - const mockClientStoreWithoutRegistration: OAuthRegisteredClientsStore = { - async getClient(_clientId: string): Promise { - return undefined; - } - // No registerClient method - }; - - describe('Handler creation', () => { - it('throws error if client store does not support registration', () => { - const options: ClientRegistrationHandlerOptions = { - clientsStore: mockClientStoreWithoutRegistration - }; - - expect(() => clientRegistrationHandler(options)).toThrow('does not support registering clients'); - }); - - it('creates handler if client store supports registration', () => { - const options: ClientRegistrationHandlerOptions = { - clientsStore: mockClientStoreWithRegistration - }; - - expect(() => clientRegistrationHandler(options)).not.toThrow(); - }); - }); - - describe('Request handling', () => { - let app: express.Express; - let spyRegisterClient: MockInstance; - - beforeEach(() => { - // Setup express app with registration handler - app = express(); - const options: ClientRegistrationHandlerOptions = { - clientsStore: mockClientStoreWithRegistration, - clientSecretExpirySeconds: 86400 // 1 day for testing - }; - - app.use('/register', clientRegistrationHandler(options)); - - // Spy on the registerClient method - spyRegisterClient = vi.spyOn(mockClientStoreWithRegistration, 'registerClient'); - }); - - afterEach(() => { - spyRegisterClient.mockRestore(); - }); - - it('requires POST method', async () => { - const response = await supertest(app) - .get('/register') - .send({ - redirect_uris: ['https://example.com/callback'] - }); - - expect(response.status).toBe(405); - expect(response.headers.allow).toBe('POST'); - expect(response.body).toEqual({ - error: 'method_not_allowed', - error_description: 'The method GET is not allowed for this endpoint' - }); - expect(spyRegisterClient).not.toHaveBeenCalled(); - }); - - it('validates required client metadata', async () => { - const response = await supertest(app).post('/register').send({ - // Missing redirect_uris (required) - client_name: 'Test Client' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_client_metadata'); - expect(spyRegisterClient).not.toHaveBeenCalled(); - }); - - it('validates redirect URIs format', async () => { - const response = await supertest(app) - .post('/register') - .send({ - redirect_uris: ['invalid-url'] // Invalid URL format - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_client_metadata'); - expect(response.body.error_description).toContain('redirect_uris'); - expect(spyRegisterClient).not.toHaveBeenCalled(); - }); - - it('successfully registers client with minimal metadata', async () => { - const clientMetadata: OAuthClientMetadata = { - redirect_uris: ['https://example.com/callback'] - }; - - const response = await supertest(app).post('/register').send(clientMetadata); - - expect(response.status).toBe(201); - - // Verify the generated client information - expect(response.body.client_id).toBeDefined(); - expect(response.body.client_secret).toBeDefined(); - expect(response.body.client_id_issued_at).toBeDefined(); - expect(response.body.client_secret_expires_at).toBeDefined(); - expect(response.body.redirect_uris).toEqual(['https://example.com/callback']); - - // Verify client was registered - expect(spyRegisterClient).toHaveBeenCalledTimes(1); - }); - - it('sets client_secret to undefined for token_endpoint_auth_method=none', async () => { - const clientMetadata: OAuthClientMetadata = { - redirect_uris: ['https://example.com/callback'], - token_endpoint_auth_method: 'none' - }; - - const response = await supertest(app).post('/register').send(clientMetadata); - - expect(response.status).toBe(201); - expect(response.body.client_secret).toBeUndefined(); - expect(response.body.client_secret_expires_at).toBeUndefined(); - }); - - it('sets client_secret_expires_at for public clients only', async () => { - // Test for public client (token_endpoint_auth_method not 'none') - const publicClientMetadata: OAuthClientMetadata = { - redirect_uris: ['https://example.com/callback'], - token_endpoint_auth_method: 'client_secret_basic' - }; - - const publicResponse = await supertest(app).post('/register').send(publicClientMetadata); - - expect(publicResponse.status).toBe(201); - expect(publicResponse.body.client_secret).toBeDefined(); - expect(publicResponse.body.client_secret_expires_at).toBeDefined(); - - // Test for non-public client (token_endpoint_auth_method is 'none') - const nonPublicClientMetadata: OAuthClientMetadata = { - redirect_uris: ['https://example.com/callback'], - token_endpoint_auth_method: 'none' - }; - - const nonPublicResponse = await supertest(app).post('/register').send(nonPublicClientMetadata); - - expect(nonPublicResponse.status).toBe(201); - expect(nonPublicResponse.body.client_secret).toBeUndefined(); - expect(nonPublicResponse.body.client_secret_expires_at).toBeUndefined(); - }); - - it('sets expiry based on clientSecretExpirySeconds', async () => { - // Create handler with custom expiry time - const customApp = express(); - const options: ClientRegistrationHandlerOptions = { - clientsStore: mockClientStoreWithRegistration, - clientSecretExpirySeconds: 3600 // 1 hour - }; - - customApp.use('/register', clientRegistrationHandler(options)); - - const response = await supertest(customApp) - .post('/register') - .send({ - redirect_uris: ['https://example.com/callback'] - }); - - expect(response.status).toBe(201); - - // Verify the expiration time (~1 hour from now) - const issuedAt = response.body.client_id_issued_at; - const expiresAt = response.body.client_secret_expires_at; - expect(expiresAt - issuedAt).toBe(3600); - }); - - it('sets no expiry when clientSecretExpirySeconds=0', async () => { - // Create handler with no expiry - const customApp = express(); - const options: ClientRegistrationHandlerOptions = { - clientsStore: mockClientStoreWithRegistration, - clientSecretExpirySeconds: 0 // No expiry - }; - - customApp.use('/register', clientRegistrationHandler(options)); - - const response = await supertest(customApp) - .post('/register') - .send({ - redirect_uris: ['https://example.com/callback'] - }); - - expect(response.status).toBe(201); - expect(response.body.client_secret_expires_at).toBe(0); - }); - - it('sets no client_id when clientIdGeneration=false', async () => { - // Create handler with no expiry - const customApp = express(); - const options: ClientRegistrationHandlerOptions = { - clientsStore: mockClientStoreWithRegistration, - clientIdGeneration: false - }; - - customApp.use('/register', clientRegistrationHandler(options)); - - const response = await supertest(customApp) - .post('/register') - .send({ - redirect_uris: ['https://example.com/callback'] - }); - - expect(response.status).toBe(201); - expect(response.body.client_id).toBeUndefined(); - expect(response.body.client_id_issued_at).toBeUndefined(); - }); - - it('handles client with all metadata fields', async () => { - const fullClientMetadata: OAuthClientMetadata = { - redirect_uris: ['https://example.com/callback'], - token_endpoint_auth_method: 'client_secret_basic', - grant_types: ['authorization_code', 'refresh_token'], - response_types: ['code'], - client_name: 'Test Client', - client_uri: 'https://example.com', - logo_uri: 'https://example.com/logo.png', - scope: 'profile email', - contacts: ['dev@example.com'], - tos_uri: 'https://example.com/tos', - policy_uri: 'https://example.com/privacy', - jwks_uri: 'https://example.com/jwks', - software_id: 'test-software', - software_version: '1.0.0' - }; - - const response = await supertest(app).post('/register').send(fullClientMetadata); - - expect(response.status).toBe(201); - - // Verify all metadata was preserved - Object.entries(fullClientMetadata).forEach(([key, value]) => { - expect(response.body[key]).toEqual(value); - }); - }); - - it('includes CORS headers in response', async () => { - const response = await supertest(app) - .post('/register') - .set('Origin', 'https://example.com') - .send({ +describe('clientRegistrationHandler (web)', () => { + it('returns 201 and client info when registration is supported', async () => { + const clientsStore: OAuthRegisteredClientsStore = { + async getClient() { + return undefined; + }, + async registerClient(client: Omit) { + // In real implementation, server may generate ids; here return minimal. + return { + ...client, + client_id: 'generated-client', + client_id_issued_at: Math.floor(Date.now() / 1000), + redirect_uris: (client as any).redirect_uris ?? [] + } as unknown as OAuthClientInformationFull; + } + }; + + const handler = clientRegistrationHandler({ clientsStore, rateLimit: false }); + + const res = await handler( + new Request('http://localhost/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ redirect_uris: ['https://example.com/callback'] - }); + }) + }) + ); - expect(response.header['access-control-allow-origin']).toBe('*'); - }); + expect(res.status).toBe(201); + const body = (await res.json()) as { client_id?: string }; + expect(body.client_id).toBeDefined(); }); }); diff --git a/packages/server/test/server/auth/handlers/revoke.test.ts b/packages/server/test/server/auth/handlers/revoke.test.ts index 61ff51b24..d9598bb43 100644 --- a/packages/server/test/server/auth/handlers/revoke.test.ts +++ b/packages/server/test/server/auth/handlers/revoke.test.ts @@ -1,233 +1,72 @@ -import type { AuthInfo, OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; -import { InvalidTokenError } from '@modelcontextprotocol/core'; -import type { Response } from 'express'; -import express from 'express'; -import supertest from 'supertest'; -import type { MockInstance } from 'vitest'; +import type { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { RevocationHandlerOptions } from '../../../../src/server/auth/handlers/revoke.js'; import { revocationHandler } from '../../../../src/server/auth/handlers/revoke.js'; import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; -describe('Revocation Handler', () => { - // Mock client data - const validClient: OAuthClientInformationFull = { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - - // Mock client store - const mockClientStore: OAuthRegisteredClientsStore = { - async getClient(clientId: string): Promise { - if (clientId === 'valid-client') { - return validClient; +describe('revocationHandler (web)', () => { + it('returns 200 on successful revocation', async () => { + const validClient: OAuthClientInformationFull = { + client_id: 'valid-client', + client_secret: 'valid-secret', + redirect_uris: ['https://example.com/callback'] + }; + + const clientsStore: OAuthRegisteredClientsStore = { + async getClient(clientId: string) { + return clientId === 'valid-client' ? validClient : undefined; } - return undefined; - } - }; - - // Mock provider with revocation capability - const mockProviderWithRevocation: OAuthServerProvider = { - clientsStore: mockClientStore, - - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise { - res.redirect('https://example.com/callback?code=mock_auth_code'); - }, - - async challengeForAuthorizationCode(): Promise { - return 'mock_challenge'; - }, - - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - - async verifyAccessToken(token: string): Promise { - if (token === 'valid_token') { + }; + + const provider: OAuthServerProvider = { + clientsStore, + async authorize(_client: OAuthClientInformationFull, _params: AuthorizationParams): Promise { + return Response.redirect('https://example.com', 302); + }, + async challengeForAuthorizationCode(): Promise { + return 'mock'; + }, + async exchangeAuthorizationCode(): Promise { return { - token, - clientId: 'valid-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 + access_token: 'mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'mock_refresh_token' }; - } - throw new InvalidTokenError('Token is invalid or expired'); - }, - - async revokeToken(_client: OAuthClientInformationFull, _request: OAuthTokenRevocationRequest): Promise { - // Success - do nothing in mock - } - }; - - // Mock provider without revocation capability - const mockProviderWithoutRevocation: OAuthServerProvider = { - clientsStore: mockClientStore, - - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise { - res.redirect('https://example.com/callback?code=mock_auth_code'); - }, - - async challengeForAuthorizationCode(): Promise { - return 'mock_challenge'; - }, - - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - - async verifyAccessToken(token: string): Promise { - if (token === 'valid_token') { + }, + async exchangeRefreshToken(): Promise { return { - token, - clientId: 'valid-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 + access_token: 'new_mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'new_mock_refresh_token' }; + }, + async verifyAccessToken() { + throw new Error('not used'); + }, + async revokeToken(_client: OAuthClientInformationFull, _request: OAuthTokenRevocationRequest): Promise { + // ok } - throw new InvalidTokenError('Token is invalid or expired'); - } - - // No revokeToken method - }; - - describe('Handler creation', () => { - it('throws error if provider does not support token revocation', () => { - const options: RevocationHandlerOptions = { provider: mockProviderWithoutRevocation }; - expect(() => revocationHandler(options)).toThrow('does not support revoking tokens'); - }); - - it('creates handler if provider supports token revocation', () => { - const options: RevocationHandlerOptions = { provider: mockProviderWithRevocation }; - expect(() => revocationHandler(options)).not.toThrow(); - }); - }); - - describe('Request handling', () => { - let app: express.Express; - let spyRevokeToken: MockInstance; - - beforeEach(() => { - // Setup express app with revocation handler - app = express(); - const options: RevocationHandlerOptions = { provider: mockProviderWithRevocation }; - app.use('/revoke', revocationHandler(options)); - - // Spy on the revokeToken method - spyRevokeToken = vi.spyOn(mockProviderWithRevocation, 'revokeToken'); - }); - - afterEach(() => { - spyRevokeToken.mockRestore(); - }); - - it('requires POST method', async () => { - const response = await supertest(app).get('/revoke').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - token: 'token_to_revoke' - }); - - expect(response.status).toBe(405); - expect(response.headers.allow).toBe('POST'); - expect(response.body).toEqual({ - error: 'method_not_allowed', - error_description: 'The method GET is not allowed for this endpoint' - }); - expect(spyRevokeToken).not.toHaveBeenCalled(); - }); - - it('requires token parameter', async () => { - const response = await supertest(app).post('/revoke').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret' - // Missing token - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_request'); - expect(spyRevokeToken).not.toHaveBeenCalled(); - }); - - it('authenticates client before revoking token', async () => { - const response = await supertest(app).post('/revoke').type('form').send({ - client_id: 'invalid-client', - client_secret: 'wrong-secret', - token: 'token_to_revoke' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_client'); - expect(spyRevokeToken).not.toHaveBeenCalled(); - }); - - it('successfully revokes token', async () => { - const response = await supertest(app).post('/revoke').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - token: 'token_to_revoke' - }); - - expect(response.status).toBe(200); - expect(response.body).toEqual({}); // Empty response on success - expect(spyRevokeToken).toHaveBeenCalledTimes(1); - expect(spyRevokeToken).toHaveBeenCalledWith(validClient, { - token: 'token_to_revoke' - }); - }); - - it('accepts optional token_type_hint', async () => { - const response = await supertest(app).post('/revoke').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - token: 'token_to_revoke', - token_type_hint: 'refresh_token' - }); - - expect(response.status).toBe(200); - expect(spyRevokeToken).toHaveBeenCalledWith(validClient, { - token: 'token_to_revoke', - token_type_hint: 'refresh_token' - }); - }); - - it('includes CORS headers in response', async () => { - const response = await supertest(app).post('/revoke').type('form').set('Origin', 'https://example.com').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - token: 'token_to_revoke' - }); - - expect(response.header['access-control-allow-origin']).toBe('*'); - }); + }; + + const handler = revocationHandler({ provider, rateLimit: false }); + + const body = new URLSearchParams({ + client_id: 'valid-client', + client_secret: 'valid-secret', + token: 'token_to_revoke' + }).toString(); + + const res = await handler( + new Request('http://localhost/revoke', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body + }) + ); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({}); }); }); diff --git a/packages/server/test/server/auth/handlers/token.test.ts b/packages/server/test/server/auth/handlers/token.test.ts index 02eab891f..d0cb0ca24 100644 --- a/packages/server/test/server/auth/handlers/token.test.ts +++ b/packages/server/test/server/auth/handlers/token.test.ts @@ -1,481 +1,116 @@ -import type { AuthInfo, OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; -import { InvalidGrantError, InvalidTokenError } from '@modelcontextprotocol/core'; -import type { Response } from 'express'; -import express from 'express'; +import type { AuthInfo, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; +import { InvalidGrantError } from '@modelcontextprotocol/core'; import * as pkceChallenge from 'pkce-challenge'; -import supertest from 'supertest'; -import { type Mock } from 'vitest'; import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { TokenHandlerOptions } from '../../../../src/server/auth/handlers/token.js'; import { tokenHandler } from '../../../../src/server/auth/handlers/token.js'; import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; -import { ProxyOAuthServerProvider } from '../../../../src/server/auth/providers/proxyProvider.js'; -// Mock pkce-challenge vi.mock('pkce-challenge', () => ({ - verifyChallenge: vi.fn().mockImplementation(async (verifier, challenge) => { - return verifier === 'valid_verifier' && challenge === 'mock_challenge'; - }) + verifyChallenge: vi.fn() })); -const mockTokens = { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' -}; - -const mockTokensWithIdToken = { - ...mockTokens, - id_token: 'mock_id_token' -}; - -describe('Token Handler', () => { - // Mock client data +describe('tokenHandler (web)', () => { const validClient: OAuthClientInformationFull = { client_id: 'valid-client', client_secret: 'valid-secret', redirect_uris: ['https://example.com/callback'] }; - // Mock client store - const mockClientStore: OAuthRegisteredClientsStore = { - async getClient(clientId: string): Promise { - if (clientId === 'valid-client') { - return validClient; - } - return undefined; + const clientsStore: OAuthRegisteredClientsStore = { + async getClient(clientId: string) { + return clientId === 'valid-client' ? validClient : undefined; } }; - // Mock provider - let mockProvider: OAuthServerProvider; - let app: express.Express; - - beforeEach(() => { - // Create fresh mocks for each test - mockProvider = { - clientsStore: mockClientStore, - - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise { - res.redirect('https://example.com/callback?code=mock_auth_code'); - }, - - async challengeForAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise { - if (authorizationCode === 'valid_code') { - return 'mock_challenge'; - } else if (authorizationCode === 'expired_code') { - throw new InvalidGrantError('The authorization code has expired'); - } - throw new InvalidGrantError('The authorization code is invalid'); - }, - - async exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise { - if (authorizationCode === 'valid_code') { - return mockTokens; - } - throw new InvalidGrantError('The authorization code is invalid or has expired'); - }, - - async exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, scopes?: string[]): Promise { - if (refreshToken === 'valid_refresh_token') { - const response: OAuthTokens = { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - - if (scopes) { - response.scope = scopes.join(' '); - } - - return response; - } - throw new InvalidGrantError('The refresh token is invalid or has expired'); - }, - - async verifyAccessToken(token: string): Promise { - if (token === 'valid_token') { - return { - token, - clientId: 'valid-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 - }; - } - throw new InvalidTokenError('Token is invalid or expired'); - }, - - async revokeToken(_client: OAuthClientInformationFull, _request: OAuthTokenRevocationRequest): Promise { - // Do nothing in mock - } - }; - - // Mock PKCE verification - (pkceChallenge.verifyChallenge as Mock).mockImplementation(async (verifier: string, challenge: string) => { - return verifier === 'valid_verifier' && challenge === 'mock_challenge'; - }); - - // Setup express app with token handler - app = express(); - const options: TokenHandlerOptions = { provider: mockProvider }; - app.use('/token', tokenHandler(options)); - }); - - describe('Basic request validation', () => { - it('requires POST method', async () => { - const response = await supertest(app).get('/token').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code' - }); - - expect(response.status).toBe(405); - expect(response.headers.allow).toBe('POST'); - expect(response.body).toEqual({ - error: 'method_not_allowed', - error_description: 'The method GET is not allowed for this endpoint' - }); - }); - - it('requires grant_type parameter', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret' - // Missing grant_type - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_request'); - }); - - it('rejects unsupported grant types', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'password' // Unsupported grant type - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('unsupported_grant_type'); - }); - }); - - describe('Client authentication', () => { - it('requires valid client credentials', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'invalid-client', - client_secret: 'wrong-secret', - grant_type: 'authorization_code' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_client'); - }); - - it('accepts valid client credentials', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'valid_verifier' - }); - - expect(response.status).toBe(200); - }); - }); - - describe('Authorization code grant', () => { - it('requires code parameter', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - // Missing code - code_verifier: 'valid_verifier' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_request'); - }); - - it('requires code_verifier parameter', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code' - // Missing code_verifier - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_request'); - }); - - it('verifies code_verifier against challenge', async () => { - // Setup invalid verifier - (pkceChallenge.verifyChallenge as Mock).mockResolvedValueOnce(false); - - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'invalid_verifier' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_grant'); - expect(response.body.error_description).toContain('code_verifier'); - }); - - it('rejects expired or invalid authorization codes', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'expired_code', - code_verifier: 'valid_verifier' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_grant'); - }); - - it('returns tokens for valid code exchange', async () => { - const mockExchangeCode = vi.spyOn(mockProvider, 'exchangeAuthorizationCode'); - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - resource: 'https://api.example.com/resource', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'valid_verifier' - }); - - expect(response.status).toBe(200); - expect(response.body.access_token).toBe('mock_access_token'); - expect(response.body.token_type).toBe('bearer'); - expect(response.body.expires_in).toBe(3600); - expect(response.body.refresh_token).toBe('mock_refresh_token'); - expect(mockExchangeCode).toHaveBeenCalledWith( - validClient, - 'valid_code', - undefined, // code_verifier is undefined after PKCE validation - undefined, // redirect_uri - new URL('https://api.example.com/resource') // resource parameter - ); - }); - - it('returns id token in code exchange if provided', async () => { - mockProvider.exchangeAuthorizationCode = async ( - client: OAuthClientInformationFull, - authorizationCode: string - ): Promise => { - if (authorizationCode === 'valid_code') { - return mockTokensWithIdToken; - } - throw new InvalidGrantError('The authorization code is invalid or has expired'); + const provider: OAuthServerProvider = { + clientsStore, + async authorize(_client: OAuthClientInformationFull, _params: AuthorizationParams): Promise { + return Response.redirect('https://example.com/callback?code=mock_auth_code', 302); + }, + async challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise { + if (authorizationCode === 'valid_code') return 'mock_challenge'; + throw new InvalidGrantError('The authorization code is invalid'); + }, + async exchangeAuthorizationCode(): Promise { + return { + access_token: 'mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'mock_refresh_token' }; + }, + async exchangeRefreshToken(): Promise { + return { + access_token: 'new_mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'new_mock_refresh_token' + }; + }, + async verifyAccessToken(token: string): Promise { + return { + token, + clientId: 'valid-client', + scopes: [], + expiresAt: Math.floor(Date.now() / 1000) + 3600 + }; + } + }; - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'valid_verifier' - }); - - expect(response.status).toBe(200); - expect(response.body.id_token).toBe('mock_id_token'); - }); - - it('passes through code verifier when using proxy provider', async () => { - const originalFetch = global.fetch; - - try { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve(mockTokens) - }); - - const proxyProvider = new ProxyOAuthServerProvider({ - endpoints: { - authorizationUrl: 'https://example.com/authorize', - tokenUrl: 'https://example.com/token' - }, - verifyAccessToken: async token => ({ - token, - clientId: 'valid-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 - }), - getClient: async clientId => (clientId === 'valid-client' ? validClient : undefined) - }); - - const proxyApp = express(); - const options: TokenHandlerOptions = { provider: proxyProvider }; - proxyApp.use('/token', tokenHandler(options)); - - const response = await supertest(proxyApp).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'any_verifier', - redirect_uri: 'https://example.com/callback' - }); - - expect(response.status).toBe(200); - expect(response.body.access_token).toBe('mock_access_token'); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://example.com/token', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: expect.stringContaining('code_verifier=any_verifier') - }) - ); - } finally { - global.fetch = originalFetch; - } - }); - - it('passes through redirect_uri when using proxy provider', async () => { - const originalFetch = global.fetch; - - try { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve(mockTokens) - }); - - const proxyProvider = new ProxyOAuthServerProvider({ - endpoints: { - authorizationUrl: 'https://example.com/authorize', - tokenUrl: 'https://example.com/token' - }, - verifyAccessToken: async token => ({ - token, - clientId: 'valid-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 - }), - getClient: async clientId => (clientId === 'valid-client' ? validClient : undefined) - }); - - const proxyApp = express(); - const options: TokenHandlerOptions = { provider: proxyProvider }; - proxyApp.use('/token', tokenHandler(options)); - - const redirectUri = 'https://example.com/callback'; - const response = await supertest(proxyApp).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'any_verifier', - redirect_uri: redirectUri - }); - - expect(response.status).toBe(200); - expect(response.body.access_token).toBe('mock_access_token'); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://example.com/token', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: expect.stringContaining(`redirect_uri=${encodeURIComponent(redirectUri)}`) - }) - ); - } finally { - global.fetch = originalFetch; - } - }); + beforeEach(() => { + vi.clearAllMocks(); }); - describe('Refresh token grant', () => { - it('requires refresh_token parameter', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'refresh_token' - // Missing refresh_token - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_request'); - }); - - it('rejects invalid refresh tokens', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'refresh_token', - refresh_token: 'invalid_refresh_token' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_grant'); - }); - - it('returns new tokens for valid refresh token', async () => { - const mockExchangeRefresh = vi.spyOn(mockProvider, 'exchangeRefreshToken'); - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - resource: 'https://api.example.com/resource', - grant_type: 'refresh_token', - refresh_token: 'valid_refresh_token' - }); - - expect(response.status).toBe(200); - expect(response.body.access_token).toBe('new_mock_access_token'); - expect(response.body.token_type).toBe('bearer'); - expect(response.body.expires_in).toBe(3600); - expect(response.body.refresh_token).toBe('new_mock_refresh_token'); - expect(mockExchangeRefresh).toHaveBeenCalledWith( - validClient, - 'valid_refresh_token', - undefined, // scopes - new URL('https://api.example.com/resource') // resource parameter - ); - }); - - it('respects requested scopes on refresh', async () => { - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'refresh_token', - refresh_token: 'valid_refresh_token', - scope: 'profile email' - }); - - expect(response.status).toBe(200); - expect(response.body.scope).toBe('profile email'); - }); + it('returns tokens for authorization_code grant when PKCE passes', async () => { + (pkceChallenge.verifyChallenge as unknown as ReturnType).mockResolvedValue(true); + const handler = tokenHandler({ provider, rateLimit: false }); + + const body = new URLSearchParams({ + client_id: 'valid-client', + client_secret: 'valid-secret', + grant_type: 'authorization_code', + code: 'valid_code', + code_verifier: 'valid_verifier' + }).toString(); + + const res = await handler( + new Request('http://localhost/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body + }) + ); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual( + expect.objectContaining({ + access_token: 'mock_access_token' + }) + ); }); - describe('CORS support', () => { - it('includes CORS headers in response', async () => { - const response = await supertest(app).post('/token').type('form').set('Origin', 'https://example.com').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'valid_verifier' - }); - - expect(response.header['access-control-allow-origin']).toBe('*'); - }); + it('returns 400 when PKCE fails', async () => { + (pkceChallenge.verifyChallenge as unknown as ReturnType).mockResolvedValue(false); + const handler = tokenHandler({ provider, rateLimit: false }); + + const body = new URLSearchParams({ + client_id: 'valid-client', + client_secret: 'valid-secret', + grant_type: 'authorization_code', + code: 'valid_code', + code_verifier: 'bad_verifier' + }).toString(); + + const res = await handler( + new Request('http://localhost/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body + }) + ); + + expect(res.status).toBe(400); + expect(await res.json()).toEqual(expect.objectContaining({ error: 'invalid_grant' })); }); }); diff --git a/packages/server/test/server/auth/middleware/allowedMethods.test.ts b/packages/server/test/server/auth/middleware/allowedMethods.test.ts index 40e9c3b1f..3cea847a1 100644 --- a/packages/server/test/server/auth/middleware/allowedMethods.test.ts +++ b/packages/server/test/server/auth/middleware/allowedMethods.test.ts @@ -1,77 +1,29 @@ -import type { Request, Response } from 'express'; -import express from 'express'; -import request from 'supertest'; - import { allowedMethods } from '../../../../src/server/auth/middleware/allowedMethods.js'; describe('allowedMethods', () => { - let app: express.Express; - - beforeEach(() => { - app = express(); - - // Set up a test router with a GET handler and 405 middleware - const router = express.Router(); - - router.get('/test', (req, res) => { - res.status(200).send('GET success'); - }); - - // Add method not allowed middleware for all other methods - router.all('/test', allowedMethods(['GET'])); - - app.use(router); - }); - - test('allows specified HTTP method', async () => { - const response = await request(app).get('/test'); - expect(response.status).toBe(200); - expect(response.text).toBe('GET success'); - }); - - test('returns 405 for unspecified HTTP methods', async () => { - const methods = ['post', 'put', 'delete', 'patch']; - - for (const method of methods) { - // @ts-expect-error - dynamic method call - const response = await request(app)[method]('/test'); - expect(response.status).toBe(405); - expect(response.body).toEqual({ - error: 'method_not_allowed', - error_description: `The method ${method.toUpperCase()} is not allowed for this endpoint` - }); - } + test('returns undefined for allowed HTTP method', () => { + const req = new Request('http://localhost/test', { method: 'GET' }); + const res = allowedMethods(['GET'], req); + expect(res).toBeUndefined(); }); - test('includes Allow header with specified methods', async () => { - const response = await request(app).post('/test'); - expect(response.headers.allow).toBe('GET'); - }); - - test('works with multiple allowed methods', async () => { - const multiMethodApp = express(); - const router = express.Router(); - - router.get('/multi', (req: Request, res: Response) => { - res.status(200).send('GET'); + test('returns 405 response for disallowed HTTP method', async () => { + const req = new Request('http://localhost/test', { method: 'POST' }); + const res = allowedMethods(['GET'], req); + expect(res).toBeDefined(); + expect(res!.status).toBe(405); + expect(res!.headers.get('allow')).toBe('GET'); + expect(await res!.json()).toEqual({ + error: 'method_not_allowed', + error_description: 'The method POST is not allowed for this endpoint' }); - router.post('/multi', (req: Request, res: Response) => { - res.status(200).send('POST'); - }); - router.all('/multi', allowedMethods(['GET', 'POST'])); - - multiMethodApp.use(router); - - // Allowed methods should work - const getResponse = await request(multiMethodApp).get('/multi'); - expect(getResponse.status).toBe(200); - - const postResponse = await request(multiMethodApp).post('/multi'); - expect(postResponse.status).toBe(200); + }); - // Unallowed methods should return 405 - const putResponse = await request(multiMethodApp).put('/multi'); - expect(putResponse.status).toBe(405); - expect(putResponse.headers.allow).toBe('GET, POST'); + test('supports multiple allowed methods', async () => { + const req = new Request('http://localhost/test', { method: 'PUT' }); + const res = allowedMethods(['GET', 'POST'], req); + expect(res).toBeDefined(); + expect(res!.status).toBe(405); + expect(res!.headers.get('allow')).toBe('GET, POST'); }); }); diff --git a/packages/server/test/server/auth/middleware/bearerAuth.test.ts b/packages/server/test/server/auth/middleware/bearerAuth.test.ts index 7b464bbff..9b0ead3ef 100644 --- a/packages/server/test/server/auth/middleware/bearerAuth.test.ts +++ b/packages/server/test/server/auth/middleware/bearerAuth.test.ts @@ -1,502 +1,118 @@ import type { AuthInfo } from '@modelcontextprotocol/core'; -import { CustomOAuthError, InsufficientScopeError, InvalidTokenError, ServerError } from '@modelcontextprotocol/core'; -import { createExpressResponseMock } from '@modelcontextprotocol/test-helpers'; -import type { Request, Response } from 'express'; -import type { Mock } from 'vitest'; +import { InsufficientScopeError, InvalidTokenError, ServerError } from '@modelcontextprotocol/core'; import { requireBearerAuth } from '../../../../src/server/auth/middleware/bearerAuth.js'; import type { OAuthTokenVerifier } from '../../../../src/server/auth/provider.js'; -// Mock verifier -const mockVerifyAccessToken = vi.fn(); -const mockVerifier: OAuthTokenVerifier = { - verifyAccessToken: mockVerifyAccessToken -}; - -describe('requireBearerAuth middleware', () => { - let mockRequest: Partial; - let mockResponse: Partial; - let nextFunction: Mock; +describe('requireBearerAuth (web)', () => { + const verifyAccessToken = vi.fn(); + const verifier: OAuthTokenVerifier = { verifyAccessToken }; beforeEach(() => { - mockRequest = { - headers: {} - }; - mockResponse = createExpressResponseMock(); - nextFunction = vi.fn(); - vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { vi.clearAllMocks(); }); - it('should call next when token is valid', async () => { - const validAuthInfo: AuthInfo = { - token: 'valid-token', - clientId: 'client-123', - scopes: ['read', 'write'], - expiresAt: Math.floor(Date.now() / 1000) + 3600 // Token expires in an hour - }; - mockVerifyAccessToken.mockResolvedValue(validAuthInfo); - - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('valid-token'); - expect(mockRequest.auth).toEqual(validAuthInfo); - expect(nextFunction).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - - it.each([ - [100], // Token expired 100 seconds ago - [0] // Token expires at the same time as now - ])('should reject expired tokens (expired %s seconds ago)', async (expiredSecondsAgo: number) => { - const expiresAt = Math.floor(Date.now() / 1000) - expiredSecondsAgo; - const expiredAuthInfo: AuthInfo = { - token: 'expired-token', - clientId: 'client-123', - scopes: ['read', 'write'], - expiresAt - }; - mockVerifyAccessToken.mockResolvedValue(expiredAuthInfo); - - mockRequest.headers = { - authorization: 'Bearer expired-token' - }; - - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('expired-token'); - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith('WWW-Authenticate', expect.stringContaining('Bearer error="invalid_token"')); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'invalid_token', error_description: 'Token has expired' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it.each([ - [undefined], // Token has no expiration time - [NaN] // Token has no expiration time - ])('should reject tokens with no expiration time (expiresAt: %s)', async (expiresAt: number | undefined) => { - const noExpirationAuthInfo: AuthInfo = { - token: 'no-expiration-token', - clientId: 'client-123', - scopes: ['read', 'write'], - expiresAt - }; - mockVerifyAccessToken.mockResolvedValue(noExpirationAuthInfo); - - mockRequest.headers = { - authorization: 'Bearer expired-token' - }; - - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('expired-token'); - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith('WWW-Authenticate', expect.stringContaining('Bearer error="invalid_token"')); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'invalid_token', error_description: 'Token has no expiration time' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should accept non-expired tokens', async () => { - const nonExpiredAuthInfo: AuthInfo = { - token: 'valid-token', - clientId: 'client-123', - scopes: ['read', 'write'], - expiresAt: Math.floor(Date.now() / 1000) + 3600 // Token expires in an hour - }; - mockVerifyAccessToken.mockResolvedValue(nonExpiredAuthInfo); - - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('valid-token'); - expect(mockRequest.auth).toEqual(nonExpiredAuthInfo); - expect(nextFunction).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - - it('should require specific scopes when configured', async () => { - const authInfo: AuthInfo = { - token: 'valid-token', - clientId: 'client-123', - scopes: ['read'] - }; - mockVerifyAccessToken.mockResolvedValue(authInfo); - - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - const middleware = requireBearerAuth({ - verifier: mockVerifier, - requiredScopes: ['read', 'write'] - }); - - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('valid-token'); - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.set).toHaveBeenCalledWith('WWW-Authenticate', expect.stringContaining('Bearer error="insufficient_scope"')); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'insufficient_scope', error_description: 'Insufficient scope' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should accept token with all required scopes', async () => { - const authInfo: AuthInfo = { - token: 'valid-token', - clientId: 'client-123', - scopes: ['read', 'write', 'admin'], - expiresAt: Math.floor(Date.now() / 1000) + 3600 // Token expires in an hour - }; - mockVerifyAccessToken.mockResolvedValue(authInfo); - - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - const middleware = requireBearerAuth({ - verifier: mockVerifier, - requiredScopes: ['read', 'write'] - }); - - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('valid-token'); - expect(mockRequest.auth).toEqual(authInfo); - expect(nextFunction).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - - it('should return 401 when no Authorization header is present', async () => { - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).not.toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith('WWW-Authenticate', expect.stringContaining('Bearer error="invalid_token"')); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'invalid_token', error_description: 'Missing Authorization header' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should return 401 when Authorization header format is invalid', async () => { - mockRequest.headers = { - authorization: 'InvalidFormat' + it('returns authInfo on success', async () => { + const info: AuthInfo = { + token: 't', + clientId: 'c', + scopes: ['read'], + expiresAt: Math.floor(Date.now() / 1000) + 3600 }; + verifyAccessToken.mockResolvedValue(info); - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); + const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); + const result = await requireBearerAuth(req, { verifier }); - expect(mockVerifyAccessToken).not.toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith('WWW-Authenticate', expect.stringContaining('Bearer error="invalid_token"')); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ - error: 'invalid_token', - error_description: "Invalid Authorization header format, expected 'Bearer TOKEN'" - }) - ); - expect(nextFunction).not.toHaveBeenCalled(); + expect('authInfo' in result).toBe(true); + if ('authInfo' in result) { + expect(result.authInfo).toEqual(info); + } }); - it('should return 401 when token verification fails with InvalidTokenError', async () => { - mockRequest.headers = { - authorization: 'Bearer invalid-token' - }; - - mockVerifyAccessToken.mockRejectedValue(new InvalidTokenError('Token expired')); - - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('invalid-token'); - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith('WWW-Authenticate', expect.stringContaining('Bearer error="invalid_token"')); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'invalid_token', error_description: 'Token expired' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should return 403 when access token has insufficient scopes', async () => { - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; + it('returns 401 when missing Authorization header', async () => { + const req = new Request('http://localhost/x'); + const result = await requireBearerAuth(req, { verifier }); - mockVerifyAccessToken.mockRejectedValue(new InsufficientScopeError('Required scopes: read, write')); - - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('valid-token'); - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.set).toHaveBeenCalledWith('WWW-Authenticate', expect.stringContaining('Bearer error="insufficient_scope"')); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'insufficient_scope', error_description: 'Required scopes: read, write' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); + expect('response' in result).toBe(true); + if ('response' in result) { + expect(result.response.status).toBe(401); + expect(result.response.headers.get('www-authenticate')).toContain('Bearer error="invalid_token"'); + expect(await result.response.json()).toEqual( + expect.objectContaining({ error: 'invalid_token', error_description: 'Missing Authorization header' }) + ); + } }); - it('should return 500 when a ServerError occurs', async () => { - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - mockVerifyAccessToken.mockRejectedValue(new ServerError('Internal server issue')); - - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); + it('returns 401 when verifier throws InvalidTokenError', async () => { + verifyAccessToken.mockRejectedValue(new InvalidTokenError('bad')); + const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); + const result = await requireBearerAuth(req, { verifier }); - expect(mockVerifyAccessToken).toHaveBeenCalledWith('valid-token'); - expect(mockResponse.status).toHaveBeenCalledWith(500); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'server_error', error_description: 'Internal server issue' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); + expect('response' in result).toBe(true); + if ('response' in result) { + expect(result.response.status).toBe(401); + } }); - it('should return 400 for generic OAuthError', async () => { - mockRequest.headers = { - authorization: 'Bearer valid-token' + it('returns 403 when scopes are insufficient', async () => { + const info: AuthInfo = { + token: 't', + clientId: 'c', + scopes: ['read'], + expiresAt: Math.floor(Date.now() / 1000) + 3600 }; + verifyAccessToken.mockResolvedValue(info); - mockVerifyAccessToken.mockRejectedValue(new CustomOAuthError('custom_error', 'Some OAuth error')); + const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); + const result = await requireBearerAuth(req, { verifier, requiredScopes: ['read', 'write'] }); - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('valid-token'); - expect(mockResponse.status).toHaveBeenCalledWith(400); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'custom_error', error_description: 'Some OAuth error' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); + expect('response' in result).toBe(true); + if ('response' in result) { + expect(result.response.status).toBe(403); + expect(result.response.headers.get('www-authenticate')).toContain('Bearer error="insufficient_scope"'); + expect(await result.response.json()).toEqual( + expect.objectContaining({ error: 'insufficient_scope', error_description: 'Insufficient scope' }) + ); + } }); - it('should return 500 when unexpected error occurs', async () => { - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - mockVerifyAccessToken.mockRejectedValue(new Error('Unexpected error')); + it('returns 500 when verifier throws ServerError', async () => { + verifyAccessToken.mockRejectedValue(new ServerError('boom')); + const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); + const result = await requireBearerAuth(req, { verifier }); - const middleware = requireBearerAuth({ verifier: mockVerifier }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockVerifyAccessToken).toHaveBeenCalledWith('valid-token'); - expect(mockResponse.status).toHaveBeenCalledWith(500); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: 'server_error', error_description: 'Internal Server Error' }) - ); - expect(nextFunction).not.toHaveBeenCalled(); + expect('response' in result).toBe(true); + if ('response' in result) { + expect(result.response.status).toBe(500); + } }); - describe('with requiredScopes in WWW-Authenticate header', () => { - it('should include scope in WWW-Authenticate header for 401 responses when requiredScopes is provided', async () => { - mockRequest.headers = {}; - - const middleware = requireBearerAuth({ - verifier: mockVerifier, - requiredScopes: ['read', 'write'] - }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith( - 'WWW-Authenticate', - 'Bearer error="invalid_token", error_description="Missing Authorization header", scope="read write"' - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should include scope in WWW-Authenticate header for 403 insufficient scope responses', async () => { - const authInfo: AuthInfo = { - token: 'valid-token', - clientId: 'client-123', - scopes: ['read'] - }; - mockVerifyAccessToken.mockResolvedValue(authInfo); - - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - const middleware = requireBearerAuth({ - verifier: mockVerifier, - requiredScopes: ['read', 'write'] - }); - - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.set).toHaveBeenCalledWith( - 'WWW-Authenticate', - 'Bearer error="insufficient_scope", error_description="Insufficient scope", scope="read write"' - ); - expect(nextFunction).not.toHaveBeenCalled(); + it('includes scope and resource_metadata in WWW-Authenticate when provided', async () => { + verifyAccessToken.mockRejectedValue(new InvalidTokenError('bad')); + const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); + const result = await requireBearerAuth(req, { + verifier, + requiredScopes: ['read', 'write'], + resourceMetadataUrl: 'https://example.com/.well-known/oauth-protected-resource' }); - it('should include both scope and resource_metadata in WWW-Authenticate header when both are provided', async () => { - mockRequest.headers = {}; - - const resourceMetadataUrl = 'https://api.example.com/.well-known/oauth-protected-resource'; - const middleware = requireBearerAuth({ - verifier: mockVerifier, - requiredScopes: ['admin'], - resourceMetadataUrl - }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith( - 'WWW-Authenticate', - `Bearer error="invalid_token", error_description="Missing Authorization header", scope="admin", resource_metadata="${resourceMetadataUrl}"` - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); + expect('response' in result).toBe(true); + if ('response' in result) { + const header = result.response.headers.get('www-authenticate') ?? ''; + expect(header).toContain('scope="read write"'); + expect(header).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"'); + } }); - describe('with resourceMetadataUrl', () => { - const resourceMetadataUrl = 'https://api.example.com/.well-known/oauth-protected-resource'; - - it('should include resource_metadata in WWW-Authenticate header for 401 responses', async () => { - mockRequest.headers = {}; - - const middleware = requireBearerAuth({ verifier: mockVerifier, resourceMetadataUrl }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith( - 'WWW-Authenticate', - `Bearer error="invalid_token", error_description="Missing Authorization header", resource_metadata="${resourceMetadataUrl}"` - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); + it('passes through InsufficientScopeError from verifier as 403', async () => { + verifyAccessToken.mockRejectedValue(new InsufficientScopeError('nope')); + const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); + const result = await requireBearerAuth(req, { verifier }); - it('should include resource_metadata in WWW-Authenticate header when token verification fails', async () => { - mockRequest.headers = { - authorization: 'Bearer invalid-token' - }; - - mockVerifyAccessToken.mockRejectedValue(new InvalidTokenError('Token expired')); - - const middleware = requireBearerAuth({ verifier: mockVerifier, resourceMetadataUrl }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith( - 'WWW-Authenticate', - `Bearer error="invalid_token", error_description="Token expired", resource_metadata="${resourceMetadataUrl}"` - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should include resource_metadata in WWW-Authenticate header for insufficient scope errors', async () => { - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - mockVerifyAccessToken.mockRejectedValue(new InsufficientScopeError('Required scopes: admin')); - - const middleware = requireBearerAuth({ verifier: mockVerifier, resourceMetadataUrl }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.set).toHaveBeenCalledWith( - 'WWW-Authenticate', - `Bearer error="insufficient_scope", error_description="Required scopes: admin", resource_metadata="${resourceMetadataUrl}"` - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should include resource_metadata when token is expired', async () => { - const expiredAuthInfo: AuthInfo = { - token: 'expired-token', - clientId: 'client-123', - scopes: ['read', 'write'], - expiresAt: Math.floor(Date.now() / 1000) - 100 - }; - mockVerifyAccessToken.mockResolvedValue(expiredAuthInfo); - - mockRequest.headers = { - authorization: 'Bearer expired-token' - }; - - const middleware = requireBearerAuth({ verifier: mockVerifier, resourceMetadataUrl }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.set).toHaveBeenCalledWith( - 'WWW-Authenticate', - `Bearer error="invalid_token", error_description="Token has expired", resource_metadata="${resourceMetadataUrl}"` - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should include resource_metadata when scope check fails', async () => { - const authInfo: AuthInfo = { - token: 'valid-token', - clientId: 'client-123', - scopes: ['read'] - }; - mockVerifyAccessToken.mockResolvedValue(authInfo); - - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - const middleware = requireBearerAuth({ - verifier: mockVerifier, - requiredScopes: ['read', 'write'], - resourceMetadataUrl - }); - - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.set).toHaveBeenCalledWith( - 'WWW-Authenticate', - `Bearer error="insufficient_scope", error_description="Insufficient scope", scope="read write", resource_metadata="${resourceMetadataUrl}"` - ); - expect(nextFunction).not.toHaveBeenCalled(); - }); - - it('should not affect server errors (no WWW-Authenticate header)', async () => { - mockRequest.headers = { - authorization: 'Bearer valid-token' - }; - - mockVerifyAccessToken.mockRejectedValue(new ServerError('Internal server issue')); - - const middleware = requireBearerAuth({ verifier: mockVerifier, resourceMetadataUrl }); - await middleware(mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockResponse.status).toHaveBeenCalledWith(500); - expect(mockResponse.set).not.toHaveBeenCalledWith('WWW-Authenticate', expect.anything()); - expect(nextFunction).not.toHaveBeenCalled(); - }); + expect('response' in result).toBe(true); + if ('response' in result) { + expect(result.response.status).toBe(403); + } }); }); diff --git a/packages/server/test/server/auth/middleware/clientAuth.test.ts b/packages/server/test/server/auth/middleware/clientAuth.test.ts index 55a00f0c2..0ee9aae0a 100644 --- a/packages/server/test/server/auth/middleware/clientAuth.test.ts +++ b/packages/server/test/server/auth/middleware/clientAuth.test.ts @@ -1,12 +1,11 @@ import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import express from 'express'; -import supertest from 'supertest'; +import { InvalidClientError, InvalidRequestError } from '@modelcontextprotocol/core'; import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; import type { ClientAuthenticationMiddlewareOptions } from '../../../../src/server/auth/middleware/clientAuth.js'; import { authenticateClient } from '../../../../src/server/auth/middleware/clientAuth.js'; -describe('clientAuth middleware', () => { +describe('authenticateClient', () => { // Mock client store const mockClientStore: OAuthRegisteredClientsStore = { async getClient(clientId: string): Promise { @@ -35,100 +34,92 @@ describe('clientAuth middleware', () => { } }; - // Setup Express app with middleware - let app: express.Express; let options: ClientAuthenticationMiddlewareOptions; beforeEach(() => { - app = express(); - app.use(express.json()); - options = { clientsStore: mockClientStore }; - - // Setup route with client auth - app.post('/protected', authenticateClient(options), (req, res) => { - res.status(200).json({ success: true, client: req.client }); - }); }); it('authenticates valid client credentials', async () => { - const response = await supertest(app).post('/protected').send({ - client_id: 'valid-client', - client_secret: 'valid-secret' - }); - - expect(response.status).toBe(200); - expect(response.body.success).toBe(true); - expect(response.body.client.client_id).toBe('valid-client'); + const client = await authenticateClient( + { + client_id: 'valid-client', + client_secret: 'valid-secret' + }, + options + ); + + expect(client.client_id).toBe('valid-client'); }); it('rejects invalid client_id', async () => { - const response = await supertest(app).post('/protected').send({ - client_id: 'non-existent-client', - client_secret: 'some-secret' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_client'); - expect(response.body.error_description).toBe('Invalid client_id'); + await expect( + authenticateClient( + { + client_id: 'non-existent-client', + client_secret: 'some-secret' + }, + options + ) + ).rejects.toBeInstanceOf(InvalidClientError); }); it('rejects invalid client_secret', async () => { - const response = await supertest(app).post('/protected').send({ - client_id: 'valid-client', - client_secret: 'wrong-secret' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_client'); - expect(response.body.error_description).toBe('Invalid client_secret'); + await expect( + authenticateClient( + { + client_id: 'valid-client', + client_secret: 'wrong-secret' + }, + options + ) + ).rejects.toBeInstanceOf(InvalidClientError); }); it('rejects missing client_id', async () => { - const response = await supertest(app).post('/protected').send({ - client_secret: 'valid-secret' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_request'); + await expect( + authenticateClient( + { + client_secret: 'valid-secret' + }, + options + ) + ).rejects.toBeInstanceOf(InvalidRequestError); }); it('allows missing client_secret if client has none', async () => { - const response = await supertest(app).post('/protected').send({ - client_id: 'expired-client' - }); - - // Since the client has no secret, this should pass without providing one - expect(response.status).toBe(200); + const client = await authenticateClient( + { + client_id: 'expired-client' + }, + options + ); + expect(client.client_id).toBe('expired-client'); }); it('rejects request when client secret has expired', async () => { - const response = await supertest(app).post('/protected').send({ - client_id: 'client-with-expired-secret', - client_secret: 'expired-secret' - }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('invalid_client'); - expect(response.body.error_description).toBe('Client secret has expired'); - }); - - it('handles malformed request body', async () => { - const response = await supertest(app).post('/protected').send('not-json-format'); - - expect(response.status).toBe(400); + await expect( + authenticateClient( + { + client_id: 'client-with-expired-secret', + client_secret: 'expired-secret' + }, + options + ) + ).rejects.toBeInstanceOf(InvalidClientError); }); - // Testing request with extra fields to ensure they're ignored it('ignores extra fields in request', async () => { - const response = await supertest(app).post('/protected').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - extra_field: 'should be ignored' - }); - - expect(response.status).toBe(200); + const client = await authenticateClient( + { + client_id: 'valid-client', + client_secret: 'valid-secret', + extra_field: 'ignored' + }, + options + ); + expect(client.client_id).toBe('valid-client'); }); }); diff --git a/packages/server/test/server/auth/providers/proxyProvider.test.ts b/packages/server/test/server/auth/providers/proxyProvider.test.ts index 375179e5b..143cfa78d 100644 --- a/packages/server/test/server/auth/providers/proxyProvider.test.ts +++ b/packages/server/test/server/auth/providers/proxyProvider.test.ts @@ -1,6 +1,5 @@ import type { AuthInfo, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; import { InsufficientScopeError, InvalidTokenError, ServerError } from '@modelcontextprotocol/core'; -import type { Response } from 'express'; import { type Mock } from 'vitest'; import type { ProxyOptions } from '../../../../src/server/auth/providers/proxyProvider.js'; @@ -14,11 +13,6 @@ describe('Proxy OAuth Server Provider', () => { redirect_uris: ['https://example.com/callback'] }; - // Mock response object - const mockResponse = { - redirect: vi.fn() - } as unknown as Response; - // Mock provider functions const mockVerifyToken = vi.fn(); const mockGetClient = vi.fn(); @@ -81,17 +75,13 @@ describe('Proxy OAuth Server Provider', () => { describe('authorization', () => { it('redirects to authorization endpoint with correct parameters', async () => { - await provider.authorize( - validClient, - { - redirectUri: 'https://example.com/callback', - codeChallenge: 'test-challenge', - state: 'test-state', - scopes: ['read', 'write'], - resource: new URL('https://api.example.com/resource') - }, - mockResponse - ); + const response = await provider.authorize(validClient, { + redirectUri: 'https://example.com/callback', + codeChallenge: 'test-challenge', + state: 'test-state', + scopes: ['read', 'write'], + resource: new URL('https://api.example.com/resource') + }); const expectedUrl = new URL('https://auth.example.com/authorize'); expectedUrl.searchParams.set('client_id', 'test-client'); @@ -103,7 +93,8 @@ describe('Proxy OAuth Server Provider', () => { expectedUrl.searchParams.set('scope', 'read write'); expectedUrl.searchParams.set('resource', 'https://api.example.com/resource'); - expect(mockResponse.redirect).toHaveBeenCalledWith(expectedUrl.toString()); + expect(response.status).toBe(302); + expect(response.headers.get('location')).toBe(expectedUrl.toString()); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e127340c..ed80684a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,9 +98,6 @@ catalogs: express: specifier: ^5.0.1 version: 5.1.0 - express-rate-limit: - specifier: ^7.5.0 - version: 7.5.1 hono: specifier: ^4.11.1 version: 4.11.1 @@ -302,6 +299,12 @@ importers: '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server + '@modelcontextprotocol/server-express': + specifier: workspace:^ + version: link:../../packages/server-express + '@modelcontextprotocol/server-hono': + specifier: workspace:^ + version: link:../../packages/server-hono cors: specifier: catalog:runtimeServerOnly version: 2.8.5 @@ -339,6 +342,9 @@ importers: '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server + '@modelcontextprotocol/server-express': + specifier: workspace:^ + version: link:../../packages/server-express express: specifier: catalog:runtimeServerOnly version: 5.1.0 @@ -552,24 +558,9 @@ importers: packages/server: dependencies: - '@hono/node-server': - specifier: catalog:runtimeServerOnly - version: 1.19.7(hono@4.11.1) content-type: specifier: catalog:runtimeServerOnly version: 1.0.5 - cors: - specifier: catalog:runtimeServerOnly - version: 2.8.5 - express: - specifier: catalog:runtimeServerOnly - version: 5.1.0 - express-rate-limit: - specifier: catalog:runtimeServerOnly - version: 7.5.1(express@5.1.0) - hono: - specifier: catalog:runtimeServerOnly - version: 4.11.1 pkce-challenge: specifier: catalog:runtimeShared version: 5.0.0 @@ -659,6 +650,119 @@ importers: specifier: catalog:devTools version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + packages/server-express: + dependencies: + '@modelcontextprotocol/core': + specifier: workspace:^ + version: link:../core + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../server + express: + specifier: catalog:runtimeServerOnly + version: 5.1.0 + devDependencies: + '@eslint/js': + specifier: catalog:devTools + version: 9.39.1 + '@modelcontextprotocol/eslint-config': + specifier: workspace:^ + version: link:../../common/eslint-config + '@modelcontextprotocol/tsconfig': + specifier: workspace:^ + version: link:../../common/tsconfig + '@modelcontextprotocol/vitest-config': + specifier: workspace:^ + version: link:../../common/vitest-config + '@types/express': + specifier: catalog:devTools + version: 5.0.5 + '@types/express-serve-static-core': + specifier: catalog:devTools + version: 5.1.0 + '@types/supertest': + specifier: catalog:devTools + version: 6.0.3 + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20251218.3 + eslint: + specifier: catalog:devTools + version: 9.39.1 + eslint-config-prettier: + specifier: catalog:devTools + version: 10.1.8(eslint@9.39.1) + eslint-plugin-n: + specifier: catalog:devTools + version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + prettier: + specifier: catalog:devTools + version: 3.6.2 + supertest: + specifier: catalog:devTools + version: 7.1.4 + tsdown: + specifier: catalog:devTools + version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + typescript: + specifier: catalog:devTools + version: 5.9.3 + typescript-eslint: + specifier: catalog:devTools + version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + vitest: + specifier: catalog:devTools + version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + + packages/server-hono: + dependencies: + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../server + hono: + specifier: catalog:runtimeServerOnly + version: 4.11.1 + devDependencies: + '@eslint/js': + specifier: catalog:devTools + version: 9.39.1 + '@modelcontextprotocol/eslint-config': + specifier: workspace:^ + version: link:../../common/eslint-config + '@modelcontextprotocol/tsconfig': + specifier: workspace:^ + version: link:../../common/tsconfig + '@modelcontextprotocol/vitest-config': + specifier: workspace:^ + version: link:../../common/vitest-config + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20251218.3 + eslint: + specifier: catalog:devTools + version: 9.39.1 + eslint-config-prettier: + specifier: catalog:devTools + version: 10.1.8(eslint@9.39.1) + eslint-plugin-n: + specifier: catalog:devTools + version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + prettier: + specifier: catalog:devTools + version: 3.6.2 + tsdown: + specifier: catalog:devTools + version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + typescript: + specifier: catalog:devTools + version: 5.9.3 + typescript-eslint: + specifier: catalog:devTools + version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + vitest: + specifier: catalog:devTools + version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + test/helpers: devDependencies: '@modelcontextprotocol/client': @@ -700,6 +804,9 @@ importers: '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server + '@modelcontextprotocol/server-express': + specifier: workspace:^ + version: link:../../packages/server-express '@modelcontextprotocol/test-helpers': specifier: workspace:^ version: link:../helpers @@ -2089,12 +2196,6 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} @@ -2464,10 +2565,6 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -3614,7 +3711,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -4762,10 +4859,6 @@ snapshots: expect-type@1.2.2: {} - express-rate-limit@7.5.1(express@5.1.0): - dependencies: - express: 5.1.0 - express@5.1.0: dependencies: accepts: 2.0.0 @@ -5172,10 +5265,6 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - js-yaml@4.1.1: dependencies: argparse: 2.0.1 diff --git a/test/integration/package.json b/test/integration/package.json index e709e431a..baa099bab 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -35,6 +35,7 @@ "@modelcontextprotocol/core": "workspace:^", "@modelcontextprotocol/client": "workspace:^", "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/server-express": "workspace:^", "zod": "catalog:runtimeShared", "vitest": "catalog:devTools", "supertest": "catalog:devTools", diff --git a/test/integration/test/issues/test_1277_zod_v4_description.test.ts b/test/integration/test/issues/test_1277_zod_v4_description.test.ts index fe58cfcd5..75a61cb36 100644 --- a/test/integration/test/issues/test_1277_zod_v4_description.test.ts +++ b/test/integration/test/issues/test_1277_zod_v4_description.test.ts @@ -9,7 +9,8 @@ import { Client } from '@modelcontextprotocol/client'; import { InMemoryTransport, ListPromptsResultSchema } from '@modelcontextprotocol/core'; import { McpServer } from '@modelcontextprotocol/server'; -import { type ZodMatrixEntry, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { zodTestMatrix } from '@modelcontextprotocol/test-helpers'; describe.each(zodTestMatrix)('Issue #1277: $zodVersionLabel', (entry: ZodMatrixEntry) => { const { z } = entry; diff --git a/test/integration/test/server.test.ts b/test/integration/test/server.test.ts index fcac6cc45..30a2c03c4 100644 --- a/test/integration/test/server.test.ts +++ b/test/integration/test/server.test.ts @@ -30,7 +30,9 @@ import { SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/core'; -import { createMcpExpressApp, InMemoryTaskMessageQueue, InMemoryTaskStore, McpServer, Server } from '@modelcontextprotocol/server'; +import { InMemoryTaskStore, McpServer, Server } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; +import type { Request, Response } from 'express'; import supertest from 'supertest'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; @@ -2066,7 +2068,7 @@ describe('createMcpExpressApp', () => { test('should parse JSON bodies', async () => { const app = createMcpExpressApp({ host: '0.0.0.0' }); // Disable host validation for this test - app.post('/test', (req, res) => { + app.post('/test', (req: Request, res: Response) => { res.json({ received: req.body }); }); @@ -2078,7 +2080,7 @@ describe('createMcpExpressApp', () => { test('should reject requests with invalid Host header by default', async () => { const app = createMcpExpressApp(); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); @@ -2097,7 +2099,7 @@ describe('createMcpExpressApp', () => { test('should allow requests with localhost Host header', async () => { const app = createMcpExpressApp(); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); @@ -2109,7 +2111,7 @@ describe('createMcpExpressApp', () => { test('should allow requests with 127.0.0.1 Host header', async () => { const app = createMcpExpressApp(); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); @@ -2121,7 +2123,7 @@ describe('createMcpExpressApp', () => { test('should not apply host validation when host is 0.0.0.0', async () => { const app = createMcpExpressApp({ host: '0.0.0.0' }); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); @@ -2134,7 +2136,7 @@ describe('createMcpExpressApp', () => { test('should apply host validation when host is explicitly localhost', async () => { const app = createMcpExpressApp({ host: 'localhost' }); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); @@ -2146,7 +2148,7 @@ describe('createMcpExpressApp', () => { test('should allow requests with IPv6 localhost Host header', async () => { const app = createMcpExpressApp(); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); @@ -2158,7 +2160,7 @@ describe('createMcpExpressApp', () => { test('should apply host validation when host is ::1 (IPv6 localhost)', async () => { const app = createMcpExpressApp({ host: '::1' }); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); @@ -2185,7 +2187,7 @@ describe('createMcpExpressApp', () => { test('should use custom allowedHosts when provided', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); @@ -2205,7 +2207,7 @@ describe('createMcpExpressApp', () => { test('should override default localhost validation when allowedHosts is provided', async () => { // Even though host is localhost, we're using custom allowedHosts const app = createMcpExpressApp({ host: 'localhost', allowedHosts: ['custom.local'] }); - app.post('/test', (_req, res) => { + app.post('/test', (_req: Request, res: Response) => { res.json({ success: true }); }); diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index f7bcececc..90e7152aa 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -1,26 +1,28 @@ import { Client } from '@modelcontextprotocol/client'; -import { getDisplayName, InMemoryTaskStore, InMemoryTransport, UriTemplate } from '@modelcontextprotocol/core'; +import type { CallToolResult, Notification, TextContent } from '@modelcontextprotocol/core'; import { - type CallToolResult, CallToolResultSchema, CompleteResultSchema, ElicitRequestSchema, ErrorCode, + getDisplayName, GetPromptResultSchema, + InMemoryTaskStore, + InMemoryTransport, ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, ListToolsResultSchema, LoggingMessageNotificationSchema, - type Notification, ReadResourceResultSchema, - type TextContent, + UriTemplate, UrlElicitationRequiredError } from '@modelcontextprotocol/core'; import { completable } from '../../../../packages/server/src/server/completable.js'; import { McpServer, ResourceTemplate } from '../../../../packages/server/src/server/mcp.js'; -import { type ZodMatrixEntry, zodTestMatrix } from '../../../../packages/server/test/server/__fixtures__/zodTestMatrix.js'; +import type { ZodMatrixEntry } from '../../../../packages/server/test/server/__fixtures__/zodTestMatrix.js'; +import { zodTestMatrix } from '../../../../packages/server/test/server/__fixtures__/zodTestMatrix.js'; function createLatch() { let latch = false; diff --git a/test/integration/test/stateManagementStreamableHttp.test.ts b/test/integration/test/stateManagementStreamableHttp.test.ts index c33100efa..6839cba6b 100644 --- a/test/integration/test/stateManagementStreamableHttp.test.ts +++ b/test/integration/test/stateManagementStreamableHttp.test.ts @@ -1,5 +1,6 @@ import { randomUUID } from 'node:crypto'; -import { createServer, type Server } from 'node:http'; +import type { Server } from 'node:http'; +import { createServer } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; import { @@ -11,8 +12,8 @@ import { McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers'; -import { type ZodMatrixEntry, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { const { z } = entry; diff --git a/test/integration/test/taskLifecycle.test.ts b/test/integration/test/taskLifecycle.test.ts index 216479e93..d644db48e 100644 --- a/test/integration/test/taskLifecycle.test.ts +++ b/test/integration/test/taskLifecycle.test.ts @@ -1,5 +1,6 @@ import { randomUUID } from 'node:crypto'; -import { createServer, type Server } from 'node:http'; +import type { Server } from 'node:http'; +import { createServer } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; import type { TaskRequestOptions } from '@modelcontextprotocol/server'; diff --git a/test/integration/test/taskResumability.test.ts b/test/integration/test/taskResumability.test.ts index 8dfd3a65a..5947649e4 100644 --- a/test/integration/test/taskResumability.test.ts +++ b/test/integration/test/taskResumability.test.ts @@ -1,5 +1,6 @@ import { randomUUID } from 'node:crypto'; -import { createServer, type Server } from 'node:http'; +import type { Server } from 'node:http'; +import { createServer } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; import { diff --git a/test/integration/test/title.test.ts b/test/integration/test/title.test.ts index 4eec82335..97348c117 100644 --- a/test/integration/test/title.test.ts +++ b/test/integration/test/title.test.ts @@ -1,7 +1,8 @@ import { Client } from '@modelcontextprotocol/client'; import { InMemoryTransport } from '@modelcontextprotocol/core'; import { McpServer, ResourceTemplate, Server } from '@modelcontextprotocol/server'; -import { type ZodMatrixEntry, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { zodTestMatrix } from '@modelcontextprotocol/test-helpers'; describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { const { z } = entry; diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index f69a602fd..666fc0509 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -8,6 +8,7 @@ "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/server-express": ["./node_modules/@modelcontextprotocol/server-express/src/index.ts"], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"], "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"] } From 354fb43bc376a6df543066c3c882a7ce776ec949 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sat, 20 Dec 2025 13:39:20 +0200 Subject: [PATCH 02/23] remove rate limiting from core server, move to express only --- packages/server-express/README.md | 4 +- packages/server-express/package.json | 3 +- packages/server-express/src/auth/router.ts | 50 ++++++++++++++++-- .../test/server/auth/router.test.ts | 30 +++++++++++ .../src/server/auth/handlers/authorize.ts | 37 ++----------- .../src/server/auth/handlers/register.ts | 52 +------------------ .../server/src/server/auth/handlers/revoke.ts | 51 ++---------------- .../server/src/server/auth/handlers/token.ts | 49 ++--------------- packages/server/src/server/auth/router.ts | 2 - packages/server/src/server/auth/web.ts | 48 ----------------- .../server/auth/handlers/authorize.test.ts | 4 +- .../server/auth/handlers/register.test.ts | 2 +- .../test/server/auth/handlers/revoke.test.ts | 2 +- .../test/server/auth/handlers/token.test.ts | 4 +- pnpm-lock.yaml | 26 ++++++++-- pnpm-workspace.yaml | 2 +- 16 files changed, 121 insertions(+), 245 deletions(-) diff --git a/packages/server-express/README.md b/packages/server-express/README.md index c1b10a9c7..7721cb16e 100644 --- a/packages/server-express/README.md +++ b/packages/server-express/README.md @@ -60,7 +60,9 @@ app.use(express.json()); app.use( mcpAuthRouter({ provider, - issuerUrl: new URL('https://auth.example.com') + issuerUrl: new URL('https://auth.example.com'), + // Optional rate limiting (implemented via express-rate-limit) + rateLimit: { windowMs: 60_000, max: 60 } }) ); ``` diff --git a/packages/server-express/package.json b/packages/server-express/package.json index d7d31cb1e..8979c37e3 100644 --- a/packages/server-express/package.json +++ b/packages/server-express/package.json @@ -43,7 +43,8 @@ }, "dependencies": { "@modelcontextprotocol/server": "workspace:^", - "express": "catalog:runtimeServerOnly" + "express": "catalog:runtimeServerOnly", + "express-rate-limit": "catalog:runtimeServerOnly" }, "devDependencies": { "@modelcontextprotocol/tsconfig": "workspace:^", diff --git a/packages/server-express/src/auth/router.ts b/packages/server-express/src/auth/router.ts index c50d2007d..b367dc46c 100644 --- a/packages/server-express/src/auth/router.ts +++ b/packages/server-express/src/auth/router.ts @@ -3,9 +3,14 @@ import { Readable } from 'node:stream'; import { URL } from 'node:url'; import type { AuthMetadataOptions, AuthRouterOptions, WebHandlerContext } from '@modelcontextprotocol/server'; -import { mcpAuthMetadataRouter as createWebAuthMetadataRouter, mcpAuthRouter as createWebAuthRouter } from '@modelcontextprotocol/server'; +import { + mcpAuthMetadataRouter as createWebAuthMetadataRouter, + mcpAuthRouter as createWebAuthRouter, + TooManyRequestsError +} from '@modelcontextprotocol/server'; import type { RequestHandler, Response as ExpressResponse } from 'express'; import express from 'express'; +import { rateLimit } from 'express-rate-limit'; type ExpressRequestLike = IncomingMessage & { method: string; @@ -112,11 +117,23 @@ async function writeWebResponse(res: ExpressResponse, webResponse: Response): Pr function toHandlerContext(req: ExpressRequestLike): WebHandlerContext { return { - parsedBody: req.body, - clientAddress: req.ip + parsedBody: req.body }; } +export type ExpressAuthRateLimitOptions = + | false + | { + /** + * Window size in ms (default: 60s) + */ + windowMs?: number; + /** + * Max requests per window per client (default: 60) + */ + max?: number; + }; + /** * Express router adapter for the Web-standard `mcpAuthRouter` from `@modelcontextprotocol/server`. * @@ -126,12 +143,34 @@ function toHandlerContext(req: ExpressRequestLike): WebHandlerContext { * app.use(mcpAuthRouter(...)) * ``` */ -export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler { +export function mcpAuthRouter(options: AuthRouterOptions & { rateLimit?: ExpressAuthRateLimitOptions }): RequestHandler { const web = createWebAuthRouter(options); const router = express.Router(); + const rateLimitOptions = options.rateLimit; + const limiter = + rateLimitOptions === false + ? undefined + : rateLimit({ + windowMs: rateLimitOptions?.windowMs ?? 60_000, + max: rateLimitOptions?.max ?? 60, + standardHeaders: true, + legacyHeaders: false, + handler: (_req, res) => { + const err = new TooManyRequestsError('Too many requests'); + res.status(429).json(err.toResponseObject()); + } + }); + + const isRateLimitedPath = (path: string): boolean => + path === '/authorize' || path === '/token' || path === '/register' || path === '/revoke'; + for (const route of web.routes) { - router.all(route.path, async (req, res, next) => { + const handlers: RequestHandler[] = []; + if (limiter && isRateLimitedPath(route.path)) { + handlers.push(limiter); + } + handlers.push(async (req, res, next) => { try { const parsedBodyProvided = (req as ExpressRequestLike).body !== undefined; const webReq = await expressToWebRequest(req as ExpressRequestLike, parsedBodyProvided); @@ -141,6 +180,7 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler { next(err); } }); + router.all(route.path, ...handlers); } return router; diff --git a/packages/server-express/test/server/auth/router.test.ts b/packages/server-express/test/server/auth/router.test.ts index 7a6c09690..9d7638543 100644 --- a/packages/server-express/test/server/auth/router.test.ts +++ b/packages/server-express/test/server/auth/router.test.ts @@ -325,6 +325,36 @@ describe('MCP Auth Router', () => { expect(response.status).not.toBe(404); }); + it('applies rate limiting to token endpoint (express-rate-limit)', async () => { + // Fresh app with a very low rate limit so we can trigger it deterministically + const limitedApp = express(); + const options = { + provider: mockProvider, + issuerUrl: new URL('https://auth.example.com'), + rateLimit: { windowMs: 60_000, max: 1 } + } as const; + limitedApp.use(mcpAuthRouter(options)); + + const first = await supertest(limitedApp).post('/token').type('form').send({ + client_id: 'valid-client', + client_secret: 'valid-secret', + grant_type: 'authorization_code', + code: 'valid_code', + code_verifier: 'valid_verifier' + }); + expect(first.status).not.toBe(404); + + const second = await supertest(limitedApp).post('/token').type('form').send({ + client_id: 'valid-client', + client_secret: 'valid-secret', + grant_type: 'authorization_code', + code: 'valid_code', + code_verifier: 'valid_verifier' + }); + expect(second.status).toBe(429); + expect(second.body).toEqual(expect.objectContaining({ error: 'too_many_requests' })); + }); + it('routes to registration endpoint', async () => { const response = await supertest(app) .post('/register') diff --git a/packages/server/src/server/auth/handlers/authorize.ts b/packages/server/src/server/auth/handlers/authorize.ts index df2702f88..ecffee114 100644 --- a/packages/server/src/server/auth/handlers/authorize.ts +++ b/packages/server/src/server/auth/handlers/authorize.ts @@ -1,17 +1,12 @@ -import { InvalidClientError, InvalidRequestError, OAuthError, ServerError, TooManyRequestsError } from '@modelcontextprotocol/core'; +import { InvalidClientError, InvalidRequestError, OAuthError, ServerError } from '@modelcontextprotocol/core'; import * as z from 'zod/v4'; import type { OAuthServerProvider } from '../provider.js'; import type { WebHandler } from '../web.js'; -import { getClientAddress, getParsedBody, InMemoryRateLimiter, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; +import { getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; export type AuthorizationHandlerOptions = { provider: OAuthServerProvider; - /** - * Rate limiting configuration for the authorization endpoint. - * Set to false to disable rate limiting for this endpoint. - */ - rateLimit?: Partial<{ windowMs: number; max: number }> | false; }; // Parameters that must be validated in order to issue redirects. @@ -33,36 +28,10 @@ const RequestAuthorizationParamsSchema = z.object({ resource: z.string().url().optional() }); -export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): WebHandler { - const limiter = - rateLimitConfig === false - ? undefined - : new InMemoryRateLimiter({ - windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1000, - max: rateLimitConfig?.max ?? 100 - }); - +export function authorizationHandler({ provider }: AuthorizationHandlerOptions): WebHandler { return async (req, ctx) => { const noStore = noStoreHeaders(); - // Rate limit by client address where possible (best-effort). - if (limiter) { - const key = `${getClientAddress(req, ctx) ?? 'global'}:authorize`; - const rl = limiter.consume(key); - if (!rl.allowed) { - return jsonResponse( - new TooManyRequestsError('You have exceeded the rate limit for authorization requests').toResponseObject(), - { - status: 429, - headers: { - ...noStore, - ...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {}) - } - } - ); - } - } - if (req.method !== 'GET' && req.method !== 'POST') { const resp = methodNotAllowedResponse(req, ['GET', 'POST']); const body = await resp.text(); diff --git a/packages/server/src/server/auth/handlers/register.ts b/packages/server/src/server/auth/handlers/register.ts index fa54644c3..4433a1b5b 100644 --- a/packages/server/src/server/auth/handlers/register.ts +++ b/packages/server/src/server/auth/handlers/register.ts @@ -1,26 +1,11 @@ import crypto from 'node:crypto'; import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import { - InvalidClientMetadataError, - OAuthClientMetadataSchema, - OAuthError, - ServerError, - TooManyRequestsError -} from '@modelcontextprotocol/core'; +import { InvalidClientMetadataError, OAuthClientMetadataSchema, OAuthError, ServerError } from '@modelcontextprotocol/core'; import type { OAuthRegisteredClientsStore } from '../clients.js'; import type { WebHandler } from '../web.js'; -import { - corsHeaders, - corsPreflightResponse, - getClientAddress, - getParsedBody, - InMemoryRateLimiter, - jsonResponse, - methodNotAllowedResponse, - noStoreHeaders -} from '../web.js'; +import { corsHeaders, corsPreflightResponse, getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; export type ClientRegistrationHandlerOptions = { /** @@ -35,13 +20,6 @@ export type ClientRegistrationHandlerOptions = { */ clientSecretExpirySeconds?: number; - /** - * Rate limiting configuration for the client registration endpoint. - * Set to false to disable rate limiting for this endpoint. - * Registration endpoints are particularly sensitive to abuse and should be rate limited. - */ - rateLimit?: Partial<{ windowMs: number; max: number }> | false; - /** * Whether to generate a client ID before calling the client registration endpoint. * @@ -55,21 +33,12 @@ const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days export function clientRegistrationHandler({ clientsStore, clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS, - rateLimit: rateLimitConfig, clientIdGeneration = true }: ClientRegistrationHandlerOptions): WebHandler { if (!clientsStore.registerClient) { throw new Error('Client registration store does not support registering clients'); } - const limiter = - rateLimitConfig === false - ? undefined - : new InMemoryRateLimiter({ - windowMs: rateLimitConfig?.windowMs ?? 60 * 60 * 1000, - max: rateLimitConfig?.max ?? 20 - }); - const cors = { allowOrigin: '*', allowMethods: ['POST', 'OPTIONS'], @@ -92,23 +61,6 @@ export function clientRegistrationHandler({ }); } - if (limiter) { - const key = `${getClientAddress(req, ctx) ?? 'global'}:register`; - const rl = limiter.consume(key); - if (!rl.allowed) { - return jsonResponse( - new TooManyRequestsError('You have exceeded the rate limit for client registration requests').toResponseObject(), - { - status: 429, - headers: { - ...baseHeaders, - ...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {}) - } - } - ); - } - } - try { const rawBody = await getParsedBody(req, ctx); const parseResult = OAuthClientMetadataSchema.safeParse(rawBody); diff --git a/packages/server/src/server/auth/handlers/revoke.ts b/packages/server/src/server/auth/handlers/revoke.ts index 30c611cff..e4814345d 100644 --- a/packages/server/src/server/auth/handlers/revoke.ts +++ b/packages/server/src/server/auth/handlers/revoke.ts @@ -1,47 +1,19 @@ -import { - InvalidRequestError, - OAuthError, - OAuthTokenRevocationRequestSchema, - ServerError, - TooManyRequestsError -} from '@modelcontextprotocol/core'; +import { InvalidRequestError, OAuthError, OAuthTokenRevocationRequestSchema, ServerError } from '@modelcontextprotocol/core'; import { authenticateClient } from '../middleware/clientAuth.js'; import type { OAuthServerProvider } from '../provider.js'; import type { WebHandler } from '../web.js'; -import { - corsHeaders, - corsPreflightResponse, - getClientAddress, - getParsedBody, - InMemoryRateLimiter, - jsonResponse, - methodNotAllowedResponse, - noStoreHeaders -} from '../web.js'; +import { corsHeaders, corsPreflightResponse, getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; export type RevocationHandlerOptions = { provider: OAuthServerProvider; - /** - * Rate limiting configuration for the token revocation endpoint. - * Set to false to disable rate limiting for this endpoint. - */ - rateLimit?: Partial<{ windowMs: number; max: number }> | false; }; -export function revocationHandler({ provider, rateLimit: rateLimitConfig }: RevocationHandlerOptions): WebHandler { +export function revocationHandler({ provider }: RevocationHandlerOptions): WebHandler { if (!provider.revokeToken) { throw new Error('Auth provider does not support revoking tokens'); } - const limiter = - rateLimitConfig === false - ? undefined - : new InMemoryRateLimiter({ - windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1000, - max: rateLimitConfig?.max ?? 50 - }); - const cors = { allowOrigin: '*', allowMethods: ['POST', 'OPTIONS'], @@ -64,23 +36,6 @@ export function revocationHandler({ provider, rateLimit: rateLimitConfig }: Revo }); } - if (limiter) { - const key = `${getClientAddress(req, ctx) ?? 'global'}:revoke`; - const rl = limiter.consume(key); - if (!rl.allowed) { - return jsonResponse( - new TooManyRequestsError('You have exceeded the rate limit for token revocation requests').toResponseObject(), - { - status: 429, - headers: { - ...baseHeaders, - ...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {}) - } - } - ); - } - } - try { const rawBody = await getParsedBody(req, ctx); const parseResult = OAuthTokenRevocationRequestSchema.safeParse(rawBody); diff --git a/packages/server/src/server/auth/handlers/token.ts b/packages/server/src/server/auth/handlers/token.ts index 096a10ff3..6dcdfd8b1 100644 --- a/packages/server/src/server/auth/handlers/token.ts +++ b/packages/server/src/server/auth/handlers/token.ts @@ -1,35 +1,14 @@ -import { - InvalidGrantError, - InvalidRequestError, - OAuthError, - ServerError, - TooManyRequestsError, - UnsupportedGrantTypeError -} from '@modelcontextprotocol/core'; +import { InvalidGrantError, InvalidRequestError, OAuthError, ServerError, UnsupportedGrantTypeError } from '@modelcontextprotocol/core'; import { verifyChallenge } from 'pkce-challenge'; import * as z from 'zod/v4'; import { authenticateClient } from '../middleware/clientAuth.js'; import type { OAuthServerProvider } from '../provider.js'; import type { WebHandler } from '../web.js'; -import { - corsHeaders, - corsPreflightResponse, - getClientAddress, - getParsedBody, - InMemoryRateLimiter, - jsonResponse, - methodNotAllowedResponse, - noStoreHeaders -} from '../web.js'; +import { corsHeaders, corsPreflightResponse, getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; export type TokenHandlerOptions = { provider: OAuthServerProvider; - /** - * Rate limiting configuration for the token endpoint. - * Set to false to disable rate limiting for this endpoint. - */ - rateLimit?: Partial<{ windowMs: number; max: number }> | false; }; const TokenRequestSchema = z.object({ @@ -49,15 +28,7 @@ const RefreshTokenGrantSchema = z.object({ resource: z.string().url().optional() }); -export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHandlerOptions): WebHandler { - const limiter = - rateLimitConfig === false - ? undefined - : new InMemoryRateLimiter({ - windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1000, - max: rateLimitConfig?.max ?? 50 - }); - +export function tokenHandler({ provider }: TokenHandlerOptions): WebHandler { const cors = { allowOrigin: '*', allowMethods: ['POST', 'OPTIONS'], @@ -80,20 +51,6 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand }); } - if (limiter) { - const key = `${getClientAddress(req, ctx) ?? 'global'}:token`; - const rl = limiter.consume(key); - if (!rl.allowed) { - return jsonResponse(new TooManyRequestsError('You have exceeded the rate limit for token requests').toResponseObject(), { - status: 429, - headers: { - ...baseHeaders, - ...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {}) - } - }); - } - } - try { const rawBody = await getParsedBody(req, ctx); const parseResult = TokenRequestSchema.safeParse(rawBody); diff --git a/packages/server/src/server/auth/router.ts b/packages/server/src/server/auth/router.ts index 083657250..61ed79806 100644 --- a/packages/server/src/server/auth/router.ts +++ b/packages/server/src/server/auth/router.ts @@ -141,8 +141,6 @@ export const createOAuthMetadata = (options: { * Installs standard MCP authorization server endpoints, including dynamic client registration and token revocation (if supported). * Also advertises standard authorization server metadata, for easier discovery of supported configurations by clients. * Note: if your MCP server is only a resource server and not an authorization server, use mcpAuthMetadataRouter instead. - * - * By default, rate limiting is applied to all endpoints to prevent abuse. */ export function mcpAuthRouter(options: AuthRouterOptions): WebAuthRouter { const oauthMetadata = createOAuthMetadata(options); diff --git a/packages/server/src/server/auth/web.ts b/packages/server/src/server/auth/web.ts index a16ada4ef..e461e9711 100644 --- a/packages/server/src/server/auth/web.ts +++ b/packages/server/src/server/auth/web.ts @@ -8,11 +8,6 @@ export type WebHandlerContext = { * If provided, handlers will use this instead of reading from the Request stream. */ parsedBody?: unknown; - - /** - * Optional client address for rate limiting (e.g., IP). - */ - clientAddress?: string; }; export type WebHandler = (req: Request, ctx?: WebHandlerContext) => Promise; @@ -32,13 +27,6 @@ export function noStoreHeaders(): HeaderMap { return { 'Cache-Control': 'no-store' }; } -export function getClientAddress(req: Request, ctx?: WebHandlerContext): string | undefined { - if (ctx?.clientAddress) return ctx.clientAddress; - const xff = req.headers.get('x-forwarded-for'); - if (xff) return xff.split(',')[0]?.trim(); - return undefined; -} - export async function getParsedBody(req: Request, ctx?: WebHandlerContext): Promise { if (ctx?.parsedBody !== undefined) { return ctx.parsedBody; @@ -102,39 +90,3 @@ export function corsPreflightResponse(options: CorsOptions): Response { headers: corsHeaders(options) }); } - -export type InMemoryRateLimitConfig = { - windowMs: number; - max: number; -}; - -type RateState = { windowStart: number; count: number }; - -/** - * Minimal in-memory rate limiter for single-process deployments. - * Not suitable for distributed setups without an external store. - */ -export class InMemoryRateLimiter { - private _state = new Map(); - - constructor(private _config: InMemoryRateLimitConfig) {} - - consume(key: string): { allowed: boolean; retryAfterSeconds?: number } { - const now = Date.now(); - const windowStart = now - (now % this._config.windowMs); - const existing = this._state.get(key); - - if (!existing || existing.windowStart !== windowStart) { - this._state.set(key, { windowStart, count: 1 }); - return { allowed: true }; - } - - if (existing.count >= this._config.max) { - const retryAfterMs = windowStart + this._config.windowMs - now; - return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1000)) }; - } - - existing.count += 1; - return { allowed: true }; - } -} diff --git a/packages/server/test/server/auth/handlers/authorize.test.ts b/packages/server/test/server/auth/handlers/authorize.test.ts index e5e65d72c..c5943915c 100644 --- a/packages/server/test/server/auth/handlers/authorize.test.ts +++ b/packages/server/test/server/auth/handlers/authorize.test.ts @@ -76,7 +76,7 @@ describe('authorizationHandler (web)', () => { }); it('redirects with a code on valid request (single redirect_uri inferred)', async () => { - const handler = authorizationHandler({ provider, rateLimit: false }); + const handler = authorizationHandler({ provider }); const res = await handler( new Request( 'http://localhost/authorize?client_id=valid-client&response_type=code&code_challenge=challenge123&code_challenge_method=S256', @@ -91,7 +91,7 @@ describe('authorizationHandler (web)', () => { }); it('requires redirect_uri if client has multiple redirect URIs', async () => { - const handler = authorizationHandler({ provider, rateLimit: false }); + const handler = authorizationHandler({ provider }); const res = await handler( new Request( 'http://localhost/authorize?client_id=multi-redirect-client&response_type=code&code_challenge=challenge123&code_challenge_method=S256', diff --git a/packages/server/test/server/auth/handlers/register.test.ts b/packages/server/test/server/auth/handlers/register.test.ts index 1d3673c3f..6a2ffcd11 100644 --- a/packages/server/test/server/auth/handlers/register.test.ts +++ b/packages/server/test/server/auth/handlers/register.test.ts @@ -20,7 +20,7 @@ describe('clientRegistrationHandler (web)', () => { } }; - const handler = clientRegistrationHandler({ clientsStore, rateLimit: false }); + const handler = clientRegistrationHandler({ clientsStore }); const res = await handler( new Request('http://localhost/register', { diff --git a/packages/server/test/server/auth/handlers/revoke.test.ts b/packages/server/test/server/auth/handlers/revoke.test.ts index d9598bb43..d960f26c3 100644 --- a/packages/server/test/server/auth/handlers/revoke.test.ts +++ b/packages/server/test/server/auth/handlers/revoke.test.ts @@ -50,7 +50,7 @@ describe('revocationHandler (web)', () => { } }; - const handler = revocationHandler({ provider, rateLimit: false }); + const handler = revocationHandler({ provider }); const body = new URLSearchParams({ client_id: 'valid-client', diff --git a/packages/server/test/server/auth/handlers/token.test.ts b/packages/server/test/server/auth/handlers/token.test.ts index d0cb0ca24..d99e4b39a 100644 --- a/packages/server/test/server/auth/handlers/token.test.ts +++ b/packages/server/test/server/auth/handlers/token.test.ts @@ -64,7 +64,7 @@ describe('tokenHandler (web)', () => { it('returns tokens for authorization_code grant when PKCE passes', async () => { (pkceChallenge.verifyChallenge as unknown as ReturnType).mockResolvedValue(true); - const handler = tokenHandler({ provider, rateLimit: false }); + const handler = tokenHandler({ provider }); const body = new URLSearchParams({ client_id: 'valid-client', @@ -92,7 +92,7 @@ describe('tokenHandler (web)', () => { it('returns 400 when PKCE fails', async () => { (pkceChallenge.verifyChallenge as unknown as ReturnType).mockResolvedValue(false); - const handler = tokenHandler({ provider, rateLimit: false }); + const handler = tokenHandler({ provider }); const body = new URLSearchParams({ client_id: 'valid-client', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed80684a8..5a91b23b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ catalogs: express: specifier: ^5.0.1 version: 5.1.0 + express-rate-limit: + specifier: ^8.2.1 + version: 8.2.1 hono: specifier: ^4.11.1 version: 4.11.1 @@ -652,15 +655,15 @@ importers: packages/server-express: dependencies: - '@modelcontextprotocol/core': - specifier: workspace:^ - version: link:../core '@modelcontextprotocol/server': specifier: workspace:^ version: link:../server express: specifier: catalog:runtimeServerOnly version: 5.1.0 + express-rate-limit: + specifier: catalog:runtimeServerOnly + version: 8.2.1(express@5.1.0) devDependencies: '@eslint/js': specifier: catalog:devTools @@ -2196,6 +2199,12 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} @@ -2434,6 +2443,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -4859,6 +4872,11 @@ snapshots: expect-type@1.2.2: {} + express-rate-limit@8.2.1(express@5.1.0): + dependencies: + express: 5.1.0 + ip-address: 10.0.1 + express@5.1.0: dependencies: accepts: 2.0.0 @@ -5132,6 +5150,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.0.1: {} + ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 12bae8326..55aac1aba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,7 +19,7 @@ catalogs: content-type: ^1.0.5 cors: ^2.8.5 express: ^5.0.1 - express-rate-limit: ^7.5.0 + express-rate-limit: ^8.2.1 raw-body: ^3.0.0 runtimeClientOnly: jose: ^6.1.1 From 564aed24ede75fba2bbee0a121ae90344aed07e2 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sat, 20 Dec 2025 13:48:27 +0200 Subject: [PATCH 03/23] rename StreamableHttpServerTransport to NodeStreamableHttpServerTransport, add server-express, server-hono to pkg.pr.new --- .github/workflows/publish.yml | 2 +- CLAUDE.md | 2 +- examples/server/README.md | 2 +- examples/server/src/elicitationFormExample.ts | 8 +-- examples/server/src/elicitationUrlExample.ts | 8 +-- .../server/src/jsonResponseStreamableHttp.ts | 8 +-- .../src/simpleStatelessStreamableHttp.ts | 4 +- examples/server/src/simpleStreamableHttp.ts | 8 +-- examples/server/src/simpleTaskInteractive.ts | 8 +-- .../sseAndStreamableHttpCompatibleServer.ts | 14 ++--- examples/server/src/ssePollingExample.ts | 6 +-- .../src/standaloneSseWithGetStreamableHttp.ts | 8 +-- packages/core/src/shared/protocol.ts | 4 +- packages/core/src/types/types.ts | 4 +- packages/server-express/README.md | 4 +- packages/server-express/package.json | 1 + packages/server-hono/package.json | 1 + packages/server/src/server/sse.ts | 2 +- packages/server/src/server/streamableHttp.ts | 12 ++--- .../src/server/webStandardStreamableHttp.ts | 2 +- .../server/test/server/streamableHttp.test.ts | 54 +++++++++---------- .../stateManagementStreamableHttp.test.ts | 8 +-- test/integration/test/taskLifecycle.test.ts | 6 +-- .../integration/test/taskResumability.test.ts | 6 +-- 24 files changed, 92 insertions(+), 90 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1167b176a..8e69fc8a7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,4 +38,4 @@ jobs: run: pnpm run build:all - name: Publish preview packages - run: pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client' + run: pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client' './packages/server-express' './packages/server-hono' diff --git a/CLAUDE.md b/CLAUDE.md index 2a0b253a6..3caca17b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -224,7 +224,7 @@ mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, extra) => { ```typescript // Server -const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); +const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); await server.connect(transport); // Client diff --git a/examples/server/README.md b/examples/server/README.md index bb1216a04..1e7322b1a 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -71,7 +71,7 @@ When deploying MCP servers in a horizontally scaled environment (multiple server ### Stateless mode -To enable stateless mode, configure the `StreamableHTTPServerTransport` with: +To enable stateless mode, configure the `NodeStreamableHTTPServerTransport` with: ```typescript sessionIdGenerator: undefined; diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index 567975662..eaeb73c32 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -9,7 +9,7 @@ import { randomUUID } from 'node:crypto'; -import { isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; @@ -322,7 +322,7 @@ async function main() { const app = createMcpExpressApp(); // Map to store transports by session ID - const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; // MCP POST endpoint const mcpPostHandler = async (req: Request, res: Response) => { @@ -332,13 +332,13 @@ async function main() { } try { - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Reuse existing transport for this session transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request - create new transport - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: sessionId => { // Store the transport by session ID when session is initialized diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index 79ba49a17..51e1344b8 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -16,7 +16,7 @@ import { getOAuthProtectedResourceMetadataUrl, isInitializeRequest, McpServer, - StreamableHTTPServerTransport, + NodeStreamableHTTPServerTransport, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; import { createMcpExpressApp, mcpAuthMetadataRouter, requireBearerAuth } from '@modelcontextprotocol/server-express'; @@ -592,7 +592,7 @@ app.post('/confirm-payment', express.urlencoded(), (req: Request, res: Response) }); // Map to store transports by session ID -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; +const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; // Interface for a function that can send an elicitation request type ElicitationSender = (params: ElicitRequestURLParams) => Promise; @@ -611,7 +611,7 @@ const mcpPostHandler = async (req: Request, res: Response) => { console.debug(`Received MCP POST for session: ${sessionId || 'unknown'}`); try { - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; @@ -619,7 +619,7 @@ const mcpPostHandler = async (req: Request, res: Response) => { const server = getServer(); // New initialization request const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore, // Enable resumability onsessioninitialized: sessionId => { diff --git a/examples/server/src/jsonResponseStreamableHttp.ts b/examples/server/src/jsonResponseStreamableHttp.ts index 44155ea9d..5935ad2c2 100644 --- a/examples/server/src/jsonResponseStreamableHttp.ts +++ b/examples/server/src/jsonResponseStreamableHttp.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; @@ -97,21 +97,21 @@ const getServer = () => { const app = createMcpExpressApp(); // Map to store transports by session ID -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; +const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; app.post('/mcp', async (req: Request, res: Response) => { console.log('Received MCP request:', req.body); try { // Check for existing session ID const sessionId = req.headers['mcp-session-id'] as string | undefined; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request - use JSON response mode - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), enableJsonResponse: true, // Enable JSON response mode onsessioninitialized: sessionId => { diff --git a/examples/server/src/simpleStatelessStreamableHttp.ts b/examples/server/src/simpleStatelessStreamableHttp.ts index 0f3a78e63..70389275c 100644 --- a/examples/server/src/simpleStatelessStreamableHttp.ts +++ b/examples/server/src/simpleStatelessStreamableHttp.ts @@ -1,5 +1,5 @@ import type { CallToolResult, GetPromptResult, ReadResourceResult } from '@modelcontextprotocol/server'; -import { McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; @@ -104,7 +104,7 @@ const app = createMcpExpressApp(); app.post('/mcp', async (req: Request, res: Response) => { const server = getServer(); try { - const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ + const transport: NodeStreamableHTTPServerTransport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await server.connect(transport); diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index f550ed7d7..c1656a544 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -17,7 +17,7 @@ import { InMemoryTaskStore, isInitializeRequest, McpServer, - StreamableHTTPServerTransport + NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp, mcpAuthMetadataRouter, requireBearerAuth } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; @@ -588,7 +588,7 @@ if (useOAuth) { } // Map to store transports by session ID -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; +const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; // MCP POST endpoint with optional auth const mcpPostHandler = async (req: Request, res: Response) => { @@ -603,14 +603,14 @@ const mcpPostHandler = async (req: Request, res: Response) => { console.log('Authenticated user:', req.auth); } try { - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore, // Enable resumability onsessioninitialized: sessionId => { diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index 4685f33f5..db4058054 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -42,7 +42,7 @@ import { ListToolsRequestSchema, RELATED_TASK_META_KEY, Server, - StreamableHTTPServerTransport + NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; @@ -642,7 +642,7 @@ const createServer = (): Server => { const app = createMcpExpressApp(); // Map to store transports by session ID -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; +const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; // Helper to check if request is initialize const isInitializeRequest = (body: unknown): boolean => { @@ -654,12 +654,12 @@ app.post('/mcp', async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; try { - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: sid => { console.log(`Session initialized: ${sid}`); diff --git a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts index 3ea3b71db..d54a5287c 100644 --- a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts +++ b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { isInitializeRequest, McpServer, SSEServerTransport, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, SSEServerTransport, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; @@ -76,7 +76,7 @@ const getServer = () => { const app = createMcpExpressApp(); // Store transports by session ID -const transports: Record = {}; +const transports: Record = {}; //============================================================================= // STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-11-25) @@ -89,16 +89,16 @@ app.all('/mcp', async (req: Request, res: Response) => { try { // Check for existing session ID const sessionId = req.headers['mcp-session-id'] as string | undefined; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Check if the transport is of the correct type const existingTransport = transports[sessionId]; - if (existingTransport instanceof StreamableHTTPServerTransport) { + if (existingTransport instanceof NodeStreamableHTTPServerTransport) { // Reuse existing transport transport = existingTransport; } else { - // Transport exists but is not a StreamableHTTPServerTransport (could be SSEServerTransport) + // Transport exists but is not a NodeStreamableHTTPServerTransport (could be SSEServerTransport) res.status(400).json({ jsonrpc: '2.0', error: { @@ -111,7 +111,7 @@ app.all('/mcp', async (req: Request, res: Response) => { } } else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) { const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore, // Enable resumability onsessioninitialized: sessionId => { @@ -186,7 +186,7 @@ app.post('/messages', async (req: Request, res: Response) => { // Reuse existing transport transport = existingTransport; } else { - // Transport exists but is not a SSEServerTransport (could be StreamableHTTPServerTransport) + // Transport exists but is not a SSEServerTransport (could be NodeStreamableHTTPServerTransport) res.status(400).json({ jsonrpc: '2.0', error: { diff --git a/examples/server/src/ssePollingExample.ts b/examples/server/src/ssePollingExample.ts index e7da09ecb..4d0841dee 100644 --- a/examples/server/src/ssePollingExample.ts +++ b/examples/server/src/ssePollingExample.ts @@ -15,7 +15,7 @@ import { randomUUID } from 'node:crypto'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import cors from 'cors'; import type { Request, Response } from 'express'; @@ -112,7 +112,7 @@ app.use(cors()); const eventStore = new InMemoryEventStore(); // Track transports by session ID for session reuse -const transports = new Map(); +const transports = new Map(); // Handle all MCP requests app.all('/mcp', async (req: Request, res: Response) => { @@ -122,7 +122,7 @@ app.all('/mcp', async (req: Request, res: Response) => { let transport = sessionId ? transports.get(sessionId) : undefined; if (!transport) { - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore, retryInterval: 2000, // Default retry interval for priming events diff --git a/examples/server/src/standaloneSseWithGetStreamableHttp.ts b/examples/server/src/standaloneSseWithGetStreamableHttp.ts index f9fb426cd..869d7e859 100644 --- a/examples/server/src/standaloneSseWithGetStreamableHttp.ts +++ b/examples/server/src/standaloneSseWithGetStreamableHttp.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto'; import type { ReadResourceResult } from '@modelcontextprotocol/server'; -import { isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; @@ -12,7 +12,7 @@ const server = new McpServer({ }); // Store transports by session ID to send notifications -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; +const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; const addResource = (name: string, content: string) => { const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`; @@ -42,14 +42,14 @@ app.post('/mcp', async (req: Request, res: Response) => { try { // Check for existing session ID const sessionId = req.headers['mcp-session-id'] as string | undefined; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: sessionId => { // Store the transport by session ID when session is initialized diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 9c65015d1..0a7e6aa1f 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -292,14 +292,14 @@ export type RequestHandlerExtra void; /** * Closes the standalone GET SSE stream, triggering client reconnection. - * Only available when using StreamableHTTPServerTransport with eventStore configured. + * Only available when using NodeStreamableHTTPServerTransport with eventStore configured. * Use this to implement polling behavior for server-initiated notifications. */ closeStandaloneSSEStream?: () => void; diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts index 35b04745d..f3e1b92a8 100644 --- a/packages/core/src/types/types.ts +++ b/packages/core/src/types/types.ts @@ -2415,13 +2415,13 @@ export interface MessageExtraInfo { /** * Callback to close the SSE stream for this request, triggering client reconnection. - * Only available when using StreamableHTTPServerTransport with eventStore configured. + * Only available when using NodeStreamableHTTPServerTransport with eventStore configured. */ closeSSEStream?: () => void; /** * Callback to close the standalone GET SSE stream, triggering client reconnection. - * Only available when using StreamableHTTPServerTransport with eventStore configured. + * Only available when using NodeStreamableHTTPServerTransport with eventStore configured. */ closeStandaloneSSEStream?: () => void; } diff --git a/packages/server-express/README.md b/packages/server-express/README.md index 7721cb16e..27fb348d7 100644 --- a/packages/server-express/README.md +++ b/packages/server-express/README.md @@ -32,13 +32,13 @@ const app = createMcpExpressApp(); // default host is 127.0.0.1; protection enab ### Streamable HTTP endpoint (Express) ```ts -import { McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; const app = createMcpExpressApp(); app.post('/mcp', async (req, res) => { - const transport = new StreamableHTTPServerTransport(); + const transport = new NodeStreamableHTTPServerTransport(); await transport.handleRequest(req, res, req.body); }); ``` diff --git a/packages/server-express/package.json b/packages/server-express/package.json index 8979c37e3..51fde8931 100644 --- a/packages/server-express/package.json +++ b/packages/server-express/package.json @@ -1,5 +1,6 @@ { "name": "@modelcontextprotocol/server-express", + "private": false, "version": "2.0.0-alpha.0", "description": "Express adapters for the Model Context Protocol TypeScript server SDK", "license": "MIT", diff --git a/packages/server-hono/package.json b/packages/server-hono/package.json index 33f633d40..ac5b01a89 100644 --- a/packages/server-hono/package.json +++ b/packages/server-hono/package.json @@ -1,5 +1,6 @@ { "name": "@modelcontextprotocol/server-hono", + "private": false, "version": "2.0.0-alpha.0", "description": "Hono adapters for the Model Context Protocol TypeScript server SDK", "license": "MIT", diff --git a/packages/server/src/server/sse.ts b/packages/server/src/server/sse.ts index 44117d0dd..06d418b2d 100644 --- a/packages/server/src/server/sse.ts +++ b/packages/server/src/server/sse.ts @@ -42,7 +42,7 @@ export interface SSEServerTransportOptions { * Server transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests. * * This transport is only available in Node.js environments. - * @deprecated SSEServerTransport is deprecated. Use StreamableHTTPServerTransport instead. + * @deprecated SSEServerTransport is deprecated. Use NodeStreamableHTTPServerTransport instead. */ export class SSEServerTransport implements Transport { private _sseResponse?: ServerResponse; diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index 354f640f9..65b39c52c 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -17,11 +17,11 @@ import type { WebStandardStreamableHTTPServerTransportOptions } from './webStand import { WebStandardStreamableHTTPServerTransport } from './webStandardStreamableHttp.js'; /** - * Configuration options for StreamableHTTPServerTransport + * Configuration options for NodeStreamableHTTPServerTransport * * This is an alias for WebStandardStreamableHTTPServerTransportOptions for backward compatibility. */ -export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions; +export type NodeStreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions; type NodeToWebRequestOptions = { parsedBody?: unknown; @@ -139,12 +139,12 @@ function writeWebResponse(res: ServerResponse, webResponse: Response): Promise randomUUID(), * }); * * // Stateless mode - explicitly set session ID to undefined - * const statelessTransport = new StreamableHTTPServerTransport({ + * const statelessTransport = new NodeStreamableHTTPServerTransport({ * sessionIdGenerator: undefined, * }); * @@ -165,10 +165,10 @@ function writeWebResponse(res: ServerResponse, webResponse: Response): Promise string) | undefined; @@ -49,7 +49,7 @@ interface TestServerConfig { /** * Helper to stop test server */ -async function stopTestServer({ server, transport }: { server: Server; transport: StreamableHTTPServerTransport }): Promise { +async function stopTestServer({ server, transport }: { server: Server; transport: NodeStreamableHTTPServerTransport }): Promise { // First close the transport to ensure all SSE streams are closed await transport.close(); @@ -153,7 +153,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { */ async function createTestServer(config: TestServerConfig = { sessionIdGenerator: () => randomUUID() }): Promise<{ server: Server; - transport: StreamableHTTPServerTransport; + transport: NodeStreamableHTTPServerTransport; mcpServer: McpServer; baseUrl: URL; }> { @@ -168,7 +168,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { } ); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: config.sessionIdGenerator, enableJsonResponse: config.enableJsonResponse ?? false, eventStore: config.eventStore, @@ -202,7 +202,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { */ async function createTestAuthServer(config: TestServerConfig = { sessionIdGenerator: () => randomUUID() }): Promise<{ server: Server; - transport: StreamableHTTPServerTransport; + transport: NodeStreamableHTTPServerTransport; mcpServer: McpServer; baseUrl: URL; }> { @@ -217,7 +217,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { } ); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: config.sessionIdGenerator, enableJsonResponse: config.enableJsonResponse ?? false, eventStore: config.eventStore, @@ -247,10 +247,10 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { } const { z } = entry; - describe('StreamableHTTPServerTransport', () => { + describe('NodeStreamableHTTPServerTransport', () => { let server: Server; let mcpServer: McpServer; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; let baseUrl: URL; let sessionId: string; @@ -979,9 +979,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); }); - describe('StreamableHTTPServerTransport with AuthInfo', () => { + describe('NodeStreamableHTTPServerTransport with AuthInfo', () => { let server: Server; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; let baseUrl: URL; let sessionId: string; @@ -1079,9 +1079,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); // Test JSON Response Mode - describe('StreamableHTTPServerTransport with JSON Response Mode', () => { + describe('NodeStreamableHTTPServerTransport with JSON Response Mode', () => { let server: Server; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; let baseUrl: URL; let sessionId: string; @@ -1166,9 +1166,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); // Test pre-parsed body handling - describe('StreamableHTTPServerTransport with pre-parsed body', () => { + describe('NodeStreamableHTTPServerTransport with pre-parsed body', () => { let server: Server; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; let baseUrl: URL; let sessionId: string; let parsedBody: unknown = null; @@ -1302,9 +1302,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); // Test resumability support - describe('StreamableHTTPServerTransport with resumability', () => { + describe('NodeStreamableHTTPServerTransport with resumability', () => { let server: Server; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; let baseUrl: URL; let sessionId: string; let mcpServer: McpServer; @@ -1538,9 +1538,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); // Test stateless mode - describe('StreamableHTTPServerTransport in stateless mode', () => { + describe('NodeStreamableHTTPServerTransport in stateless mode', () => { let server: Server; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; let baseUrl: URL; beforeEach(async () => { @@ -1626,9 +1626,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); // Test SSE priming events for POST streams - describe('StreamableHTTPServerTransport POST SSE priming events', () => { + describe('NodeStreamableHTTPServerTransport POST SSE priming events', () => { let server: Server; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; let baseUrl: URL; let sessionId: string; let mcpServer: McpServer; @@ -2327,7 +2327,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); // Test onsessionclosed callback - describe('StreamableHTTPServerTransport onsessionclosed callback', () => { + describe('NodeStreamableHTTPServerTransport onsessionclosed callback', () => { it('should call onsessionclosed callback when session is closed via DELETE', async () => { const mockCallback = vi.fn(); @@ -2486,7 +2486,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); // Test async callbacks for onsessioninitialized and onsessionclosed - describe('StreamableHTTPServerTransport async callbacks', () => { + describe('NodeStreamableHTTPServerTransport async callbacks', () => { it('should support async onsessioninitialized callback', async () => { const initializationOrder: string[] = []; @@ -2693,9 +2693,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); // Test DNS rebinding protection - describe('StreamableHTTPServerTransport DNS rebinding protection', () => { + describe('NodeStreamableHTTPServerTransport DNS rebinding protection', () => { let server: Server; - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; let baseUrl: URL; afterEach(async () => { @@ -2931,7 +2931,7 @@ async function createTestServerWithDnsProtection(config: { enableDnsRebindingProtection?: boolean; }): Promise<{ server: Server; - transport: StreamableHTTPServerTransport; + transport: NodeStreamableHTTPServerTransport; mcpServer: McpServer; baseUrl: URL; }> { @@ -2948,7 +2948,7 @@ async function createTestServerWithDnsProtection(config: { }); } - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: config.sessionIdGenerator, allowedHosts: config.allowedHosts, allowedOrigins: config.allowedOrigins, diff --git a/test/integration/test/stateManagementStreamableHttp.test.ts b/test/integration/test/stateManagementStreamableHttp.test.ts index 6839cba6b..72180b688 100644 --- a/test/integration/test/stateManagementStreamableHttp.test.ts +++ b/test/integration/test/stateManagementStreamableHttp.test.ts @@ -10,7 +10,7 @@ import { ListResourcesResultSchema, ListToolsResultSchema, McpServer, - StreamableHTTPServerTransport + NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; @@ -69,7 +69,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { ); // Create transport with or without session management - const serverTransport = new StreamableHTTPServerTransport({ + const serverTransport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: withSessionManagement ? () => randomUUID() // With session management, generate UUID : undefined // Without session management, return undefined @@ -90,7 +90,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { describe('Stateless Mode', () => { let server: Server; let mcpServer: McpServer; - let serverTransport: StreamableHTTPServerTransport; + let serverTransport: NodeStreamableHTTPServerTransport; let baseUrl: URL; beforeEach(async () => { @@ -254,7 +254,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { describe('Stateful Mode', () => { let server: Server; let mcpServer: McpServer; - let serverTransport: StreamableHTTPServerTransport; + let serverTransport: NodeStreamableHTTPServerTransport; let baseUrl: URL; beforeEach(async () => { diff --git a/test/integration/test/taskLifecycle.test.ts b/test/integration/test/taskLifecycle.test.ts index d644db48e..5e3dfc408 100644 --- a/test/integration/test/taskLifecycle.test.ts +++ b/test/integration/test/taskLifecycle.test.ts @@ -15,7 +15,7 @@ import { McpError, McpServer, RELATED_TASK_META_KEY, - StreamableHTTPServerTransport, + NodeStreamableHTTPServerTransport, TaskSchema } from '@modelcontextprotocol/server'; import { listenOnRandomPort, waitForTaskStatus } from '@modelcontextprotocol/test-helpers'; @@ -24,7 +24,7 @@ import { z } from 'zod'; describe('Task Lifecycle Integration Tests', () => { let server: Server; let mcpServer: McpServer; - let serverTransport: StreamableHTTPServerTransport; + let serverTransport: NodeStreamableHTTPServerTransport; let baseUrl: URL; let taskStore: InMemoryTaskStore; @@ -189,7 +189,7 @@ describe('Task Lifecycle Integration Tests', () => { ); // Create transport - serverTransport = new StreamableHTTPServerTransport({ + serverTransport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); diff --git a/test/integration/test/taskResumability.test.ts b/test/integration/test/taskResumability.test.ts index 5947649e4..7a1b15707 100644 --- a/test/integration/test/taskResumability.test.ts +++ b/test/integration/test/taskResumability.test.ts @@ -8,7 +8,7 @@ import { InMemoryEventStore, LoggingMessageNotificationSchema, McpServer, - StreamableHTTPServerTransport + NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; @@ -18,7 +18,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { describe('Transport resumability', () => { let server: Server; let mcpServer: McpServer; - let serverTransport: StreamableHTTPServerTransport; + let serverTransport: NodeStreamableHTTPServerTransport; let baseUrl: URL; let eventStore: InMemoryEventStore; @@ -84,7 +84,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { ); // Create a transport with the event store - serverTransport = new StreamableHTTPServerTransport({ + serverTransport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore }); From 223fcb067b2d85adc80c91cf6277c760f8a59756 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sat, 20 Dec 2025 15:19:26 +0200 Subject: [PATCH 04/23] move back to hono/node-server for mapping incoming node request to web request --- .github/workflows/publish.yml | 4 +- README.md | 3 +- examples/server/src/simpleTaskInteractive.ts | 4 +- .../sseAndStreamableHttpCompatibleServer.ts | 2 +- packages/core/src/shared/protocol.ts | 4 +- packages/server-express/package.json | 3 +- .../server-express/src/auth/bearerAuth.ts | 14 +- packages/server-express/src/auth/router.ts | 135 ++-------------- packages/server/package.json | 11 +- packages/server/src/server/streamableHttp.ts | 152 ++++-------------- .../server/test/server/streamableHttp.test.ts | 3 +- pnpm-lock.yaml | 14 ++ pnpm-workspace.yaml | 1 + test/integration/test/taskLifecycle.test.ts | 2 +- .../integration/test/taskResumability.test.ts | 2 +- 15 files changed, 85 insertions(+), 269 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8e69fc8a7..25cd12862 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,4 +38,6 @@ jobs: run: pnpm run build:all - name: Publish preview packages - run: pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client' './packages/server-express' './packages/server-hono' + run: + pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client' + './packages/server-express' './packages/server-hono' diff --git a/README.md b/README.md index dc0116c96..4d5270287 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # MCP TypeScript SDK -> [!IMPORTANT] -> **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).** +> [!IMPORTANT] **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).** > > We anticipate a stable v2 release in Q1 2026. Until then, **v1.x remains the recommended version** for production use. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade. > diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index db4058054..469ecf0c2 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -40,9 +40,9 @@ import { InMemoryTaskStore, isTerminal, ListToolsRequestSchema, + NodeStreamableHTTPServerTransport, RELATED_TASK_META_KEY, - Server, - NodeStreamableHTTPServerTransport + Server } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; diff --git a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts index d54a5287c..bb2636ea3 100644 --- a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts +++ b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { isInitializeRequest, McpServer, SSEServerTransport, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport, SSEServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 0a7e6aa1f..c9242e96d 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -292,14 +292,14 @@ export type RequestHandlerExtra void; /** * Closes the standalone GET SSE stream, triggering client reconnection. - * Only available when using NodeStreamableHTTPServerTransport with eventStore configured. + * Only available when using aStreamableHTTPServerTransport with eventStore configured. * Use this to implement polling behavior for server-initiated notifications. */ closeStandaloneSSEStream?: () => void; diff --git a/packages/server-express/package.json b/packages/server-express/package.json index 51fde8931..bca9ac505 100644 --- a/packages/server-express/package.json +++ b/packages/server-express/package.json @@ -45,7 +45,8 @@ "dependencies": { "@modelcontextprotocol/server": "workspace:^", "express": "catalog:runtimeServerOnly", - "express-rate-limit": "catalog:runtimeServerOnly" + "express-rate-limit": "catalog:runtimeServerOnly", + "@remix-run/node-fetch-server": "catalog:runtimeServerOnly" }, "devDependencies": { "@modelcontextprotocol/tsconfig": "workspace:^", diff --git a/packages/server-express/src/auth/bearerAuth.ts b/packages/server-express/src/auth/bearerAuth.ts index a923ff796..d8d0aad8b 100644 --- a/packages/server-express/src/auth/bearerAuth.ts +++ b/packages/server-express/src/auth/bearerAuth.ts @@ -3,7 +3,8 @@ import { URL } from 'node:url'; import type { AuthInfo } from '@modelcontextprotocol/core'; import type { BearerAuthMiddlewareOptions } from '@modelcontextprotocol/server'; import { requireBearerAuth as requireBearerAuthWeb } from '@modelcontextprotocol/server'; -import type { NextFunction, Request as ExpressRequest, RequestHandler, Response as ExpressResponse } from 'express'; +import { sendResponse } from '@remix-run/node-fetch-server'; +import type { NextFunction, Request as ExpressRequest, RequestHandler } from 'express'; declare module 'express-serve-static-core' { interface Request { @@ -21,15 +22,6 @@ function expressRequestUrl(req: ExpressRequest): URL { return new URL(path, `${protocol}://${host}`); } -async function writeWebResponse(res: ExpressResponse, webResponse: Response): Promise { - res.status(webResponse.status); - for (const [k, v] of webResponse.headers.entries()) { - res.setHeader(k, v); - } - const bodyText = await webResponse.text(); - res.send(bodyText); -} - /** * Express middleware wrapper for the Web-standard `requireBearerAuth` helper. * @@ -54,7 +46,7 @@ export function requireBearerAuth(options: BearerAuthMiddlewareOptions): Request return; } - await writeWebResponse(res, result.response); + await sendResponse(res, result.response); } catch (err) { next(err); } diff --git a/packages/server-express/src/auth/router.ts b/packages/server-express/src/auth/router.ts index b367dc46c..868149efd 100644 --- a/packages/server-express/src/auth/router.ts +++ b/packages/server-express/src/auth/router.ts @@ -1,126 +1,15 @@ -import type { IncomingMessage } from 'node:http'; -import { Readable } from 'node:stream'; -import { URL } from 'node:url'; - -import type { AuthMetadataOptions, AuthRouterOptions, WebHandlerContext } from '@modelcontextprotocol/server'; +import type { AuthMetadataOptions, AuthRouterOptions } from '@modelcontextprotocol/server'; import { + getParsedBody, mcpAuthMetadataRouter as createWebAuthMetadataRouter, mcpAuthRouter as createWebAuthRouter, TooManyRequestsError } from '@modelcontextprotocol/server'; -import type { RequestHandler, Response as ExpressResponse } from 'express'; +import { createRequest, sendResponse } from '@remix-run/node-fetch-server'; +import type { RequestHandler } from 'express'; import express from 'express'; import { rateLimit } from 'express-rate-limit'; -type ExpressRequestLike = IncomingMessage & { - method: string; - headers: Record; - originalUrl?: string; - url?: string; - protocol?: string; - // express adds this when trust proxy is enabled - ip?: string; - body?: unknown; - get?: (name: string) => string | undefined; -}; - -function expressRequestUrl(req: ExpressRequestLike): URL { - const host = req.get?.('host') ?? req.headers.host ?? 'localhost'; - const proto = req.protocol ?? 'http'; - const path = req.originalUrl ?? req.url ?? '/'; - return new URL(path, `${proto}://${host}`); -} - -function toHeaders(req: ExpressRequestLike): Headers { - const headers = new Headers(); - for (const [key, value] of Object.entries(req.headers)) { - if (value === undefined) continue; - if (Array.isArray(value)) { - headers.set(key, value.join(', ')); - } else { - headers.set(key, value); - } - } - return headers; -} - -async function readBody(req: IncomingMessage): Promise { - const chunks: Buffer[] = []; - for await (const chunk of req) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - } - return Buffer.concat(chunks); -} - -async function expressToWebRequest(req: ExpressRequestLike, parsedBodyProvided: boolean): Promise { - const url = expressRequestUrl(req); - const headers = toHeaders(req); - - // If upstream body parsing ran, the Node stream is likely consumed. - if (parsedBodyProvided) { - return new Request(url, { method: req.method, headers }); - } - - if (req.method === 'GET' || req.method === 'HEAD') { - return new Request(url, { method: req.method, headers }); - } - - const body = await readBody(req); - return new Request(url, { method: req.method, headers, body }); -} - -async function writeWebResponse(res: ExpressResponse, webResponse: Response): Promise { - res.status(webResponse.status); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getSetCookie = (webResponse.headers as any).getSetCookie as (() => string[]) | undefined; - const setCookies = typeof getSetCookie === 'function' ? getSetCookie.call(webResponse.headers) : undefined; - - for (const [key, value] of webResponse.headers.entries()) { - if (key.toLowerCase() === 'set-cookie' && setCookies?.length) continue; - res.setHeader(key, value); - } - - if (setCookies?.length) { - res.setHeader('set-cookie', setCookies); - } - - res.flushHeaders?.(); - - if (!webResponse.body) { - res.end(); - return; - } - - await new Promise((resolve, reject) => { - const readable = Readable.fromWeb(webResponse.body as unknown as ReadableStream); - readable.on('error', err => { - try { - res.destroy(err as Error); - } catch { - // ignore - } - reject(err); - }); - res.on('error', reject); - res.on('close', () => { - try { - readable.destroy(); - } catch { - // ignore - } - }); - readable.pipe(res); - res.on('finish', () => resolve()); - }); -} - -function toHandlerContext(req: ExpressRequestLike): WebHandlerContext { - return { - parsedBody: req.body - }; -} - export type ExpressAuthRateLimitOptions = | false | { @@ -172,10 +61,10 @@ export function mcpAuthRouter(options: AuthRouterOptions & { rateLimit?: Express } handlers.push(async (req, res, next) => { try { - const parsedBodyProvided = (req as ExpressRequestLike).body !== undefined; - const webReq = await expressToWebRequest(req as ExpressRequestLike, parsedBodyProvided); - const webRes = await route.handler(webReq, toHandlerContext(req as ExpressRequestLike)); - await writeWebResponse(res, webRes); + const webReq = createRequest(req, res); + const parsedBody = req.body !== undefined ? req.body : await getParsedBody(webReq); + const webRes = await route.handler(webReq, { parsedBody }); + await sendResponse(res, webRes); } catch (err) { next(err); } @@ -198,10 +87,10 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions): RequestHand for (const route of web.routes) { router.all(route.path, async (req, res, next) => { try { - const parsedBodyProvided = (req as ExpressRequestLike).body !== undefined; - const webReq = await expressToWebRequest(req as ExpressRequestLike, parsedBodyProvided); - const webRes = await route.handler(webReq, toHandlerContext(req as ExpressRequestLike)); - await writeWebResponse(res, webRes); + const webReq = createRequest(req, res); + const parsedBody = req.body !== undefined ? req.body : await getParsedBody(webReq); + const webRes = await route.handler(webReq, { parsedBody }); + await sendResponse(res, webRes); } catch (err) { next(err); } diff --git a/packages/server/package.json b/packages/server/package.json index 4f32cf171..20bd77aae 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -44,9 +44,10 @@ "client": "tsx scripts/cli.ts client" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "content-type": "catalog:runtimeServerOnly", - "raw-body": "catalog:runtimeServerOnly", "pkce-challenge": "catalog:runtimeShared", + "raw-body": "catalog:runtimeServerOnly", "zod": "catalog:runtimeShared", "zod-to-json-schema": "catalog:runtimeShared" }, @@ -63,13 +64,13 @@ } }, "devDependencies": { + "@cfworker/json-schema": "catalog:runtimeShared", + "@eslint/js": "catalog:devTools", "@modelcontextprotocol/core": "workspace:^", - "@modelcontextprotocol/tsconfig": "workspace:^", - "@modelcontextprotocol/vitest-config": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", "@modelcontextprotocol/test-helpers": "workspace:^", - "@cfworker/json-schema": "catalog:runtimeShared", - "@eslint/js": "catalog:devTools", + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", "@types/content-type": "catalog:devTools", "@types/cors": "catalog:devTools", "@types/cross-spawn": "catalog:devTools", diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index 65b39c52c..10a990196 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -8,143 +8,37 @@ */ import type { IncomingMessage, ServerResponse } from 'node:http'; -import { Readable } from 'node:stream'; -import { URL } from 'node:url'; +import { getRequestListener } from '@hono/node-server'; import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport } from '@modelcontextprotocol/core'; import type { WebStandardStreamableHTTPServerTransportOptions } from './webStandardStreamableHttp.js'; import { WebStandardStreamableHTTPServerTransport } from './webStandardStreamableHttp.js'; /** - * Configuration options for NodeStreamableHTTPServerTransport + * Configuration options for StreamableHTTPServerTransport * * This is an alias for WebStandardStreamableHTTPServerTransportOptions for backward compatibility. */ -export type NodeStreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions; - -type NodeToWebRequestOptions = { - parsedBody?: unknown; -}; - -function getRequestUrl(req: IncomingMessage): URL { - const host = req.headers.host ?? 'localhost'; - const isTls = Boolean((req.socket as { encrypted?: boolean } | undefined)?.encrypted); - const protocol = isTls ? 'https' : 'http'; - const path = req.url ?? '/'; - return new URL(path, `${protocol}://${host}`); -} - -function toHeaders(req: IncomingMessage): Headers { - const headers = new Headers(); - for (const [key, value] of Object.entries(req.headers)) { - if (value === undefined) continue; - if (Array.isArray(value)) { - // Preserve multi-value headers as a comma-joined value. - // (Set-Cookie does not appear on requests; this is fine here.) - headers.set(key, value.join(', ')); - } else { - headers.set(key, value); - } - } - return headers; -} - -async function readBody(req: IncomingMessage): Promise { - const chunks: Buffer[] = []; - for await (const chunk of req) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - } - return Buffer.concat(chunks); -} - -async function nodeToWebRequest(req: IncomingMessage, options?: NodeToWebRequestOptions): Promise { - const url = getRequestUrl(req); - const method = req.method ?? 'GET'; - const headers = toHeaders(req); - - // If an upstream framework already parsed the body, the IncomingMessage stream - // may be consumed; rely on parsedBody instead of trying to read again. - if (options?.parsedBody !== undefined) { - return new Request(url, { method, headers }); - } - - // Only attach bodies for methods that can carry one. - if (method === 'GET' || method === 'HEAD') { - return new Request(url, { method, headers }); - } - - const body = await readBody(req); - return new Request(url, { method, headers, body }); -} - -function writeWebResponse(res: ServerResponse, webResponse: Response): Promise { - res.statusCode = webResponse.status; - - // Prefer undici's multi Set-Cookie support when available. - // Note: must call with the correct `this` (undici brand-checks Headers). - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getSetCookie = (webResponse.headers as any).getSetCookie as (() => string[]) | undefined; - const setCookies = typeof getSetCookie === 'function' ? getSetCookie.call(webResponse.headers) : undefined; - - for (const [key, value] of webResponse.headers.entries()) { - // We'll handle Set-Cookie separately if we have structured values. - if (key.toLowerCase() === 'set-cookie' && setCookies?.length) continue; - res.setHeader(key, value); - } - - if (setCookies?.length) { - res.setHeader('set-cookie', setCookies); - } - - // Node requires writing headers before streaming body. - res.flushHeaders?.(); - - if (!webResponse.body) { - res.end(); - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - const readable = Readable.fromWeb(webResponse.body as unknown as ReadableStream); - readable.on('error', err => { - try { - res.destroy(err as Error); - } catch { - // ignore - } - reject(err); - }); - res.on('error', reject); - res.on('close', () => { - try { - readable.destroy(); - } catch { - // ignore - } - }); - readable.pipe(res); - res.on('finish', () => resolve()); - }); -} +export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions; /** * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. * It supports both SSE streaming and direct HTTP responses. * * This is a wrapper around `WebStandardStreamableHTTPServerTransport` that provides Node.js HTTP compatibility. - * It converts between Node.js HTTP (IncomingMessage/ServerResponse) and Web Standard Request/Response. + * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs. * * Usage example: * * ```typescript * // Stateful mode - server sets the session ID - * const statefulTransport = new NodeStreamableHTTPServerTransport({ + * const statefulTransport = new StreamableHTTPServerTransport({ * sessionIdGenerator: () => randomUUID(), * }); * * // Stateless mode - explicitly set session ID to undefined - * const statelessTransport = new NodeStreamableHTTPServerTransport({ + * const statelessTransport = new StreamableHTTPServerTransport({ * sessionIdGenerator: undefined, * }); * @@ -167,9 +61,23 @@ function writeWebResponse(res: ServerResponse, webResponse: Response): Promise; + // Store auth and parsedBody per request for passing through to handleRequest + private _requestContext: WeakMap = new WeakMap(); - constructor(options: NodeStreamableHTTPServerTransportOptions = {}) { + constructor(options: StreamableHTTPServerTransportOptions = {}) { this._webStandardTransport = new WebStandardStreamableHTTPServerTransport(options); + + // Create a request listener that wraps the web standard transport + // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming + this._requestListener = getRequestListener(async (webRequest: Request) => { + // Get context if available (set during handleRequest) + const context = this._requestContext.get(webRequest); + return this._webStandardTransport.handleRequest(webRequest, { + authInfo: context?.authInfo, + parsedBody: context?.parsedBody + }); + }); } /** @@ -245,13 +153,21 @@ export class NodeStreamableHTTPServerTransport implements Transport { * @param parsedBody - Optional pre-parsed body from body-parser middleware */ async handleRequest(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise { + // Store context for this request to pass through auth and parsedBody + // We need to intercept the request creation to attach this context const authInfo = req.auth; - const webRequest = await nodeToWebRequest(req, { parsedBody }); - const webResponse = await this._webStandardTransport.handleRequest(webRequest, { - authInfo, - parsedBody + + // Create a custom handler that includes our context + const handler = getRequestListener(async (webRequest: Request) => { + return this._webStandardTransport.handleRequest(webRequest, { + authInfo, + parsedBody + }); }); - await writeWebResponse(res, webResponse); + + // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion + // including proper SSE streaming support + await handler(req, res); } /** diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/server/test/server/streamableHttp.test.ts index 5a5230940..57e47668b 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/server/test/server/streamableHttp.test.ts @@ -17,7 +17,8 @@ import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers'; import { McpServer } from '../../src/server/mcp.js'; import { NodeStreamableHTTPServerTransport } from '../../src/server/streamableHttp.js'; import type { EventId, EventStore, StreamId } from '../../src/server/webStandardStreamableHttp.js'; -import { type ZodMatrixEntry, zodTestMatrix } from './__fixtures__/zodTestMatrix.js'; +import type { ZodMatrixEntry } from './__fixtures__/zodTestMatrix.js'; +import { zodTestMatrix } from './__fixtures__/zodTestMatrix.js'; async function getFreePort() { return new Promise(res => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bca9f21f7..c2b9d0835 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ catalogs: '@hono/node-server': specifier: ^1.19.7 version: 1.19.7 + '@remix-run/node-fetch-server': + specifier: ^0.13.0 + version: 0.13.0 content-type: specifier: ^1.0.5 version: 1.0.5 @@ -564,6 +567,9 @@ importers: packages/server: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.7(hono@4.11.1) content-type: specifier: catalog:runtimeServerOnly version: 1.0.5 @@ -661,6 +667,9 @@ importers: '@modelcontextprotocol/server': specifier: workspace:^ version: link:../server + '@remix-run/node-fetch-server': + specifier: catalog:runtimeServerOnly + version: 0.13.0 express: specifier: catalog:runtimeServerOnly version: 5.1.0 @@ -1207,6 +1216,9 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@remix-run/node-fetch-server@0.13.0': + resolution: {integrity: sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA==} + '@rolldown/binding-android-arm64@1.0.0-beta.53': resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3878,6 +3890,8 @@ snapshots: dependencies: quansync: 1.0.0 + '@remix-run/node-fetch-server@0.13.0': {} + '@rolldown/binding-android-arm64@1.0.0-beta.53': optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 55aac1aba..a7222dd71 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -21,6 +21,7 @@ catalogs: express: ^5.0.1 express-rate-limit: ^8.2.1 raw-body: ^3.0.0 + '@remix-run/node-fetch-server': ^0.13.0 runtimeClientOnly: jose: ^6.1.1 cross-spawn: ^7.0.5 diff --git a/test/integration/test/taskLifecycle.test.ts b/test/integration/test/taskLifecycle.test.ts index 5e3dfc408..324da6aa2 100644 --- a/test/integration/test/taskLifecycle.test.ts +++ b/test/integration/test/taskLifecycle.test.ts @@ -14,8 +14,8 @@ import { InMemoryTaskStore, McpError, McpServer, - RELATED_TASK_META_KEY, NodeStreamableHTTPServerTransport, + RELATED_TASK_META_KEY, TaskSchema } from '@modelcontextprotocol/server'; import { listenOnRandomPort, waitForTaskStatus } from '@modelcontextprotocol/test-helpers'; diff --git a/test/integration/test/taskResumability.test.ts b/test/integration/test/taskResumability.test.ts index 4e4625561..db60e2d4e 100644 --- a/test/integration/test/taskResumability.test.ts +++ b/test/integration/test/taskResumability.test.ts @@ -3,13 +3,13 @@ import type { Server } from 'node:http'; import { createServer } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server'; import { CallToolResultSchema, LoggingMessageNotificationSchema, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; From aaeff28619a2c4d93551d958db1cab3ad945f417 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sat, 20 Dec 2025 15:37:19 +0200 Subject: [PATCH 05/23] hono-server updates --- packages/server-hono/src/auth/bearerAuth.ts | 19 ++ packages/server-hono/src/auth/router.ts | 58 ++++-- packages/server-hono/src/hono.ts | 90 +++++++++ packages/server-hono/src/index.ts | 2 + packages/server-hono/src/streamableHttp.ts | 11 +- packages/server-hono/test/server-hono.test.ts | 177 +++++++++++++++++- 6 files changed, 335 insertions(+), 22 deletions(-) create mode 100644 packages/server-hono/src/auth/bearerAuth.ts create mode 100644 packages/server-hono/src/hono.ts diff --git a/packages/server-hono/src/auth/bearerAuth.ts b/packages/server-hono/src/auth/bearerAuth.ts new file mode 100644 index 000000000..1258eee11 --- /dev/null +++ b/packages/server-hono/src/auth/bearerAuth.ts @@ -0,0 +1,19 @@ +import type { BearerAuthMiddlewareOptions } from '@modelcontextprotocol/server'; +import { requireBearerAuth as requireBearerAuthWeb } from '@modelcontextprotocol/server'; +import type { MiddlewareHandler } from 'hono'; +/** + * Hono middleware wrapper for the Web-standard `requireBearerAuth` helper. + * + * On success, sets `c.set('auth', authInfo)` and calls `next()`. + * On failure, returns the JSON error response. + */ +export function requireBearerAuth(options: BearerAuthMiddlewareOptions): MiddlewareHandler { + return async (c, next) => { + const result = await requireBearerAuthWeb(c.req.raw, options); + if ('authInfo' in result) { + c.set('auth', result.authInfo); + return await next(); + } + return result.response; + }; +} diff --git a/packages/server-hono/src/auth/router.ts b/packages/server-hono/src/auth/router.ts index 4c61c1d2c..f17765318 100644 --- a/packages/server-hono/src/auth/router.ts +++ b/packages/server-hono/src/auth/router.ts @@ -1,33 +1,61 @@ import type { AuthMetadataOptions, AuthRoute, AuthRouterOptions } from '@modelcontextprotocol/server'; -import { mcpAuthMetadataRouter as createWebAuthMetadataRouter, mcpAuthRouter as createWebAuthRouter } from '@modelcontextprotocol/server'; -import type { Handler, Hono } from 'hono'; - -export type RegisterMcpAuthRoutesOptions = AuthRouterOptions; +import { + getParsedBody, + mcpAuthMetadataRouter as createWebAuthMetadataRouter, + mcpAuthRouter as createWebAuthRouter +} from '@modelcontextprotocol/server'; +import type { Handler } from 'hono'; +import { Hono } from 'hono'; /** - * Registers the standard MCP OAuth endpoints on a Hono app. + * Hono router adapter for the Web-standard `mcpAuthRouter` from `@modelcontextprotocol/server`. + * + * IMPORTANT: This router MUST be mounted at the application root. * - * IMPORTANT: These routes MUST be mounted at the application root. + * @example + * ```ts + * app.route('/', mcpAuthRouter(...)) + * ``` */ -export function registerMcpAuthRoutes(app: Hono, options: RegisterMcpAuthRoutesOptions): void { +export function mcpAuthRouter(options: AuthRouterOptions): Hono { const web = createWebAuthRouter(options); - registerRoutes(app, web.routes); + const router = new Hono(); + registerRoutes(router, web.routes); + return router; } /** - * Registers only the auth metadata endpoints (RFC 8414 + RFC 9728) on a Hono app. + * Hono router adapter for the Web-standard `mcpAuthMetadataRouter` from `@modelcontextprotocol/server`. * - * IMPORTANT: These routes MUST be mounted at the application root. + * IMPORTANT: This router MUST be mounted at the application root. */ -export function registerMcpAuthMetadataRoutes(app: Hono, options: AuthMetadataOptions): void { +export function mcpAuthMetadataRouter(options: AuthMetadataOptions): Hono { const web = createWebAuthMetadataRouter(options); - registerRoutes(app, web.routes); + const router = new Hono(); + registerRoutes(router, web.routes); + return router; } function registerRoutes(app: Hono, routes: AuthRoute[]): void { for (const route of routes) { - // Hono's `on()` expects methods like 'GET', 'POST', etc. - const handler: Handler = c => route.handler(c.req.raw); - app.on(route.methods, route.path, handler); + // Use `all()` so unsupported methods still reach the handler and can return 405, + // matching the Express adapter behavior. + const handler: Handler = async c => { + let parsedBody = c.get('parsedBody'); + if (parsedBody === undefined && c.req.method === 'POST') { + // Parse from a clone so we don't consume the original request stream. + parsedBody = await getParsedBody(c.req.raw.clone()); + } + return route.handler(c.req.raw, { parsedBody }); + }; + app.all(route.path, handler); } } + +export function registerMcpAuthRoutes(app: Hono, options: AuthRouterOptions): void { + app.route('/', mcpAuthRouter(options)); +} + +export function registerMcpAuthMetadataRoutes(app: Hono, options: AuthMetadataOptions): void { + app.route('/', mcpAuthMetadataRouter(options)); +} diff --git a/packages/server-hono/src/hono.ts b/packages/server-hono/src/hono.ts new file mode 100644 index 000000000..accf4ab27 --- /dev/null +++ b/packages/server-hono/src/hono.ts @@ -0,0 +1,90 @@ +import type { Context } from 'hono'; +import { Hono } from 'hono'; + +import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; + +/** + * Options for creating an MCP Hono application. + */ +export interface CreateMcpHonoAppOptions { + /** + * The hostname to bind to. Defaults to '127.0.0.1'. + * When set to '127.0.0.1', 'localhost', or '::1', DNS rebinding protection is automatically enabled. + */ + host?: string; + + /** + * List of allowed hostnames for DNS rebinding protection. + * If provided, host header validation will be applied using this list. + * For IPv6, provide addresses with brackets (e.g., '[::1]'). + * + * This is useful when binding to '0.0.0.0' or '::' but still wanting + * to restrict which hostnames are allowed. + */ + allowedHosts?: string[]; +} + +/** + * Creates a Hono application pre-configured for MCP servers. + * + * When the host is '127.0.0.1', 'localhost', or '::1' (the default is '127.0.0.1'), + * DNS rebinding protection middleware is automatically applied to protect against + * DNS rebinding attacks on localhost servers. + * + * This also installs a small JSON body parsing middleware (similar to `express.json()`) + * that stashes the parsed body into `c.set('parsedBody', ...)` when `Content-Type` includes + * `application/json`. + * + * @param options - Configuration options + * @returns A configured Hono application + */ +export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono { + const { host = '127.0.0.1', allowedHosts } = options; + + const app = new Hono(); + + // Similar to `express.json()`: parse JSON bodies and make them available to MCP adapters via `parsedBody`. + app.use('*', async (c: Context, next) => { + // If an upstream middleware already set parsedBody, keep it. + if (c.get('parsedBody') !== undefined) { + return await next(); + } + + const ct = c.req.header('content-type') ?? ''; + if (!ct.includes('application/json')) { + return await next(); + } + + try { + // Parse from a clone so we don't consume the original request stream. + const parsed = await c.req.raw.clone().json(); + c.set('parsedBody', parsed); + } catch { + // Mirror express.json() behavior loosely: reject invalid JSON. + return c.text('Invalid JSON', 400); + } + + return await next(); + }); + + // If allowedHosts is explicitly provided, use that for validation. + if (allowedHosts) { + app.use('*', hostHeaderValidation(allowedHosts)); + } else { + // Apply DNS rebinding protection automatically for localhost hosts. + const localhostHosts = ['127.0.0.1', 'localhost', '::1']; + if (localhostHosts.includes(host)) { + app.use('*', localhostHostValidation()); + } else if (host === '0.0.0.0' || host === '::') { + // Warn when binding to all interfaces without DNS rebinding protection. + // eslint-disable-next-line no-console + console.warn( + `Warning: Server is binding to ${host} without DNS rebinding protection. ` + + 'Consider using the allowedHosts option to restrict allowed hosts, ' + + 'or use authentication to protect your server.' + ); + } + } + + return app; +} diff --git a/packages/server-hono/src/index.ts b/packages/server-hono/src/index.ts index 5a7cb5129..bc6de4318 100644 --- a/packages/server-hono/src/index.ts +++ b/packages/server-hono/src/index.ts @@ -1,3 +1,5 @@ +export * from './auth/bearerAuth.js'; export * from './auth/router.js'; +export * from './hono.js'; export * from './middleware/hostHeaderValidation.js'; export * from './streamableHttp.js'; diff --git a/packages/server-hono/src/streamableHttp.ts b/packages/server-hono/src/streamableHttp.ts index d81960713..2da1bafcd 100644 --- a/packages/server-hono/src/streamableHttp.ts +++ b/packages/server-hono/src/streamableHttp.ts @@ -1,4 +1,5 @@ import type { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { getParsedBody } from '@modelcontextprotocol/server'; import type { Context, Handler } from 'hono'; /** @@ -10,5 +11,13 @@ import type { Context, Handler } from 'hono'; * ``` */ export function mcpStreamableHttpHandler(transport: WebStandardStreamableHTTPServerTransport): Handler { - return (c: Context) => transport.handleRequest(c.req.raw); + return async (c: Context) => { + let parsedBody = c.get('parsedBody'); + if (parsedBody === undefined && c.req.method === 'POST') { + // Parse from a clone so we don't consume the original request stream. + parsedBody = await getParsedBody(c.req.raw.clone()); + } + const authInfo = c.get('auth'); + return transport.handleRequest(c.req.raw, { authInfo, parsedBody }); + }; } diff --git a/packages/server-hono/test/server-hono.test.ts b/packages/server-hono/test/server-hono.test.ts index 8b143411b..130e11c71 100644 --- a/packages/server-hono/test/server-hono.test.ts +++ b/packages/server-hono/test/server-hono.test.ts @@ -1,22 +1,36 @@ import type { AuthorizationParams, OAuthClientInformationFull, OAuthServerProvider, OAuthTokens } from '@modelcontextprotocol/server'; +import type { Context } from 'hono'; import { Hono } from 'hono'; +import { vi } from 'vitest'; -import { registerMcpAuthRoutes } from '../src/auth/router.js'; +import { mcpAuthRouter } from '../src/auth/router.js'; +import { createMcpHonoApp } from '../src/hono.js'; import { hostHeaderValidation } from '../src/middleware/hostHeaderValidation.js'; import { mcpStreamableHttpHandler } from '../src/streamableHttp.js'; describe('@modelcontextprotocol/server-hono', () => { - test('mcpStreamableHttpHandler delegates to transport.handleRequest', async () => { - const calls: { url?: string; method?: string }[] = []; + test('mcpStreamableHttpHandler delegates to transport.handleRequest (and passes authInfo + parsedBody when set)', async () => { + const calls: { url?: string; method?: string; options?: unknown }[] = []; const transport = { - async handleRequest(req: Request): Promise { - calls.push({ url: req.url, method: req.method }); + async handleRequest(req: Request, options?: unknown): Promise { + calls.push({ url: req.url, method: req.method, options }); return new Response('ok', { status: 200, headers: { 'content-type': 'text/plain' } }); } }; const app = new Hono(); + app.use('/mcp', async (c: Context, next) => { + // Upstream middleware can pre-parse and stash body + auth. + c.set('parsedBody', { hello: 'world' }); + c.set('auth', { + token: 't', + clientId: 'c', + scopes: [], + expiresAt: Math.floor(Date.now() / 1000) + 60 + }); + return await next(); + }); app.all('/mcp', mcpStreamableHttpHandler(transport as unknown as Parameters[0])); const res = await app.request('http://localhost/mcp', { method: 'POST' }); @@ -25,6 +39,12 @@ describe('@modelcontextprotocol/server-hono', () => { expect(calls).toHaveLength(1); expect(calls[0]!.method).toBe('POST'); expect(calls[0]!.url).toBe('http://localhost/mcp'); + expect(calls[0]!.options).toEqual( + expect.objectContaining({ + parsedBody: { hello: 'world' }, + authInfo: expect.objectContaining({ clientId: 'c' }) + }) + ); }); test('hostHeaderValidation blocks invalid Host and allows valid Host', async () => { @@ -93,7 +113,7 @@ describe('@modelcontextprotocol/server-hono', () => { }; const app = new Hono(); - registerMcpAuthRoutes(app, { provider, issuerUrl: new URL('https://auth.example.com') }); + app.route('/', mcpAuthRouter({ provider, issuerUrl: new URL('https://auth.example.com') })); const metadata = await app.request('http://localhost/.well-known/oauth-authorization-server', { method: 'GET' }); expect(metadata.status).toBe(200); @@ -111,4 +131,149 @@ describe('@modelcontextprotocol/server-hono', () => { expect(location).toContain('code=mock_auth_code'); expect(location).toContain('state=s'); }); + + test('registerMcpAuthRoutes returns 405 (not 404) for unsupported methods', async () => { + const provider: OAuthServerProvider = { + clientsStore: { + async getClient() { + return undefined; + } + }, + async authorize() { + throw new Error('not used'); + }, + async challengeForAuthorizationCode() { + throw new Error('not used'); + }, + async exchangeAuthorizationCode() { + throw new Error('not used'); + }, + async exchangeRefreshToken() { + throw new Error('not used'); + }, + async verifyAccessToken() { + throw new Error('not used'); + } + }; + + const app = new Hono(); + app.route('/', mcpAuthRouter({ provider, issuerUrl: new URL('https://auth.example.com') })); + + const res = await app.request('http://localhost/authorize', { method: 'PUT' }); + expect(res.status).toBe(405); + }); + + test('registerMcpAuthRoutes passes parsedBody to web handlers (POST /authorize works with empty raw body)', async () => { + const validClient: OAuthClientInformationFull = { + client_id: 'valid-client', + client_secret: 'valid-secret', + redirect_uris: ['https://example.com/callback'] + }; + + const provider: OAuthServerProvider = { + clientsStore: { + async getClient(clientId: string) { + return clientId === 'valid-client' ? validClient : undefined; + } + }, + async authorize(_client: OAuthClientInformationFull, params: AuthorizationParams): Promise { + const u = new URL(params.redirectUri); + u.searchParams.set('code', 'mock_auth_code'); + if (params.state) u.searchParams.set('state', params.state); + return Response.redirect(u.toString(), 302); + }, + async challengeForAuthorizationCode(): Promise { + return 'mock_challenge'; + }, + async exchangeAuthorizationCode(): Promise { + return { + access_token: 'mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'mock_refresh_token' + }; + }, + async exchangeRefreshToken(): Promise { + return { + access_token: 'new_mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'new_mock_refresh_token' + }; + }, + async verifyAccessToken() { + throw new Error('not used'); + } + }; + + const app = new Hono(); + app.use('/authorize', async (c: Context, next) => { + c.set('parsedBody', { + client_id: 'valid-client', + response_type: 'code', + code_challenge: 'x', + code_challenge_method: 'S256', + redirect_uri: 'https://example.com/callback', + state: 's' + }); + return await next(); + }); + app.route('/', mcpAuthRouter({ provider, issuerUrl: new URL('https://auth.example.com') })); + + const authorize = await app.request('http://localhost/authorize', { method: 'POST' }); + expect(authorize.status).toBe(302); + const location = authorize.headers.get('location')!; + expect(location).toContain('https://example.com/callback'); + expect(location).toContain('code=mock_auth_code'); + expect(location).toContain('state=s'); + }); + + test('createMcpHonoApp enables localhost DNS rebinding protection by default', async () => { + const app = createMcpHonoApp(); + app.get('/health', c => c.text('ok')); + + const bad = await app.request('http://localhost/health', { headers: { Host: 'evil.com:3000' } }); + expect(bad.status).toBe(403); + + const good = await app.request('http://localhost/health', { headers: { Host: 'localhost:3000' } }); + expect(good.status).toBe(200); + }); + + test('createMcpHonoApp uses allowedHosts when provided (even when binding to 0.0.0.0)', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const app = createMcpHonoApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); + warn.mockRestore(); + + app.get('/health', c => c.text('ok')); + + const bad = await app.request('http://localhost/health', { headers: { Host: 'evil.com:3000' } }); + expect(bad.status).toBe(403); + + const good = await app.request('http://localhost/health', { headers: { Host: 'myapp.local:3000' } }); + expect(good.status).toBe(200); + }); + + test('createMcpHonoApp does not apply host validation for 0.0.0.0 without allowedHosts', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const app = createMcpHonoApp({ host: '0.0.0.0' }); + warn.mockRestore(); + + app.get('/health', c => c.text('ok')); + + const res = await app.request('http://localhost/health', { headers: { Host: 'evil.com:3000' } }); + expect(res.status).toBe(200); + }); + + test('createMcpHonoApp parses JSON bodies into parsedBody (express.json()-like)', async () => { + const app = createMcpHonoApp(); + app.post('/echo', (c: Context) => c.json(c.get('parsedBody'))); + + const res = await app.request('http://localhost/echo', { + method: 'POST', + headers: { Host: 'localhost:3000', 'content-type': 'application/json' }, + body: JSON.stringify({ a: 1 }) + }); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ a: 1 }); + }); }); From 4b7fcb0966b0e69dfad717d493a8ff5ca4f004bf Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 21 Dec 2025 17:14:20 +0200 Subject: [PATCH 06/23] PoC: remove sse, remove server auth, use better-auth for server auth examples, update faq --- .gitignore | 1 + docs/faq.md | 8 + examples/server/package.json | 3 +- examples/server/src/elicitationUrlExample.ts | 57 +- .../src/honoWebStandardStreamableHttp.ts | 3 +- examples/server/src/simpleSseServer.ts | 175 ----- examples/server/src/simpleStreamableHttp.ts | 67 +- .../sseAndStreamableHttpCompatibleServer.ts | 253 ------ examples/shared/package.json | 10 +- examples/shared/src/auth.ts | 82 ++ examples/shared/src/authMiddleware.ts | 125 +++ examples/shared/src/authServer.ts | 243 ++++++ .../shared/src/demoInMemoryOAuthProvider.ts | 252 ------ examples/shared/src/index.ts | 12 +- .../test/demoInMemoryOAuthProvider.test.ts | 79 +- .../server-express/src/auth/bearerAuth.ts | 54 -- packages/server-express/src/auth/router.ts | 101 --- packages/server-express/src/index.ts | 2 - .../test/server-express.test.ts | 182 +++++ .../test/server/auth/router.test.ts | 500 ------------ packages/server-hono/src/auth/bearerAuth.ts | 19 - packages/server-hono/src/auth/router.ts | 61 -- packages/server-hono/src/index.ts | 3 - packages/server-hono/src/streamableHttp.ts | 23 - packages/server-hono/test/server-hono.test.ts | 230 +----- packages/server/src/index.ts | 5 +- packages/server/src/server/auth/clients.ts | 22 - .../src/server/auth/handlers/authorize.ts | 150 ---- .../src/server/auth/handlers/metadata.ts | 33 - .../src/server/auth/handlers/register.ts | 105 --- .../server/src/server/auth/handlers/revoke.ts | 59 -- .../server/src/server/auth/handlers/token.ts | 127 --- packages/server/src/server/auth/index.ts | 13 - .../server/auth/middleware/allowedMethods.ts | 20 - .../src/server/auth/middleware/bearerAuth.ts | 101 --- .../src/server/auth/middleware/clientAuth.ts | 50 -- packages/server/src/server/auth/provider.ts | 82 -- .../server/auth/providers/proxyProvider.ts | 230 ------ packages/server/src/server/auth/router.ts | 280 ------- packages/server/src/server/auth/web.ts | 92 --- packages/server/src/server/helper/body.ts | 26 + packages/server/src/server/sse.ts | 220 ------ .../server/auth/handlers/authorize.test.ts | 104 --- .../server/auth/handlers/metadata.test.ts | 80 -- .../server/auth/handlers/register.test.ts | 39 - .../test/server/auth/handlers/revoke.test.ts | 72 -- .../test/server/auth/handlers/token.test.ts | 116 --- .../auth/middleware/allowedMethods.test.ts | 29 - .../server/auth/middleware/bearerAuth.test.ts | 118 --- .../server/auth/middleware/clientAuth.test.ts | 125 --- .../auth/providers/proxyProvider.test.ts | 334 -------- packages/server/test/server/sse.test.ts | 733 ------------------ pnpm-lock.yaml | 444 ++++++++++- pnpm-workspace.yaml | 102 +-- 54 files changed, 1262 insertions(+), 5194 deletions(-) delete mode 100644 examples/server/src/simpleSseServer.ts delete mode 100644 examples/server/src/sseAndStreamableHttpCompatibleServer.ts create mode 100644 examples/shared/src/auth.ts create mode 100644 examples/shared/src/authMiddleware.ts create mode 100644 examples/shared/src/authServer.ts delete mode 100644 examples/shared/src/demoInMemoryOAuthProvider.ts delete mode 100644 packages/server-express/src/auth/bearerAuth.ts delete mode 100644 packages/server-express/src/auth/router.ts create mode 100644 packages/server-express/test/server-express.test.ts delete mode 100644 packages/server-express/test/server/auth/router.test.ts delete mode 100644 packages/server-hono/src/auth/bearerAuth.ts delete mode 100644 packages/server-hono/src/auth/router.ts delete mode 100644 packages/server-hono/src/streamableHttp.ts delete mode 100644 packages/server/src/server/auth/clients.ts delete mode 100644 packages/server/src/server/auth/handlers/authorize.ts delete mode 100644 packages/server/src/server/auth/handlers/metadata.ts delete mode 100644 packages/server/src/server/auth/handlers/register.ts delete mode 100644 packages/server/src/server/auth/handlers/revoke.ts delete mode 100644 packages/server/src/server/auth/handlers/token.ts delete mode 100644 packages/server/src/server/auth/index.ts delete mode 100644 packages/server/src/server/auth/middleware/allowedMethods.ts delete mode 100644 packages/server/src/server/auth/middleware/bearerAuth.ts delete mode 100644 packages/server/src/server/auth/middleware/clientAuth.ts delete mode 100644 packages/server/src/server/auth/provider.ts delete mode 100644 packages/server/src/server/auth/providers/proxyProvider.ts delete mode 100644 packages/server/src/server/auth/router.ts delete mode 100644 packages/server/src/server/auth/web.ts create mode 100644 packages/server/src/server/helper/body.ts delete mode 100644 packages/server/src/server/sse.ts delete mode 100644 packages/server/test/server/auth/handlers/authorize.test.ts delete mode 100644 packages/server/test/server/auth/handlers/metadata.test.ts delete mode 100644 packages/server/test/server/auth/handlers/register.test.ts delete mode 100644 packages/server/test/server/auth/handlers/revoke.test.ts delete mode 100644 packages/server/test/server/auth/handlers/token.test.ts delete mode 100644 packages/server/test/server/auth/middleware/allowedMethods.test.ts delete mode 100644 packages/server/test/server/auth/middleware/bearerAuth.test.ts delete mode 100644 packages/server/test/server/auth/middleware/clientAuth.test.ts delete mode 100644 packages/server/test/server/auth/providers/proxyProvider.test.ts delete mode 100644 packages/server/test/server/sse.test.ts diff --git a/.gitignore b/.gitignore index a1b83bc4f..75c943a57 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,4 @@ dist/ # IDE .idea/ +.cursor/ \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md index 1afe1d10b..4e2f6cc35 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -65,6 +65,14 @@ For production use, you can either: The SDK ships several runnable server examples under `examples/server/src`. Start from the server examples index in [`examples/server/README.md`](../examples/server/README.md) and the entry-point quick start in the root [`README.md`](../README.md). +### Why did we remove `server` auth exports? + +Server authentication & authorization is outside of the scope of the SDK, and the recommendation is to use packages that focus on this area specifically (or a full-fledged Authorization Server for those who use such). Example packages provide an example with `better-auth`. + +### Why did we remove `server` SSE transport? + +The SSE transport has been deprecated for a long time, and `v2` will not support it on the server side any more. Client side will keep supporting it in order to be able to connect to legacy SSE servers via the `v2` SDK, but serving SSE from `v2` will not be possible. Servers wanting to switch to `v2` and using SSE should migrate to Streamable HTTP. + ## v1 (legacy) ### Where do v1 documentation and v1-specific fixes live? diff --git a/examples/server/package.json b/examples/server/package.json index cb37d9f40..7241f3ab2 100644 --- a/examples/server/package.json +++ b/examples/server/package.json @@ -35,13 +35,14 @@ }, "dependencies": { "@hono/node-server": "catalog:runtimeServerOnly", - "hono": "catalog:runtimeServerOnly", "@modelcontextprotocol/examples-shared": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/server-express": "workspace:^", "@modelcontextprotocol/server-hono": "workspace:^", + "better-auth": "^1.4.7", "cors": "catalog:runtimeServerOnly", "express": "catalog:runtimeServerOnly", + "hono": "catalog:runtimeServerOnly", "zod": "catalog:runtimeShared" }, "devDependencies": { diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index 51e1344b8..b3d433214 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -9,17 +9,20 @@ import { randomUUID } from 'node:crypto'; -import { setupAuthServer } from '@modelcontextprotocol/examples-shared'; -import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server'; import { - checkResourceAllowed, getOAuthProtectedResourceMetadataUrl, + mcpAuthMetadataRouter, + requireBearerAuth, + setupAuthServer +} from '@modelcontextprotocol/examples-shared'; +import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; -import { createMcpExpressApp, mcpAuthMetadataRouter, requireBearerAuth } from '@modelcontextprotocol/server-express'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; @@ -238,47 +241,6 @@ const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true }); -const tokenVerifier = { - verifyAccessToken: async (token: string) => { - const endpoint = oauthMetadata.introspection_endpoint; - - if (!endpoint) { - throw new Error('No token verification endpoint available in metadata'); - } - - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - token: token - }).toString() - }); - - if (!response.ok) { - const text = await response.text().catch(() => null); - throw new Error(`Invalid or expired token: ${text}`); - } - - const data = (await response.json()) as { aud: string; client_id: string; scope: string; exp: number }; - - if (!data.aud) { - throw new Error(`Resource Indicator (RFC8707) missing`); - } - if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) { - throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`); - } - - // Convert the response to AuthInfo format - return { - token, - clientId: data.client_id, - scopes: data.scope ? data.scope.split(' ') : [], - expiresAt: data.exp - }; - } -}; // Add metadata routes to the main MCP server app.use( mcpAuthMetadataRouter({ @@ -290,9 +252,10 @@ app.use( ); authMiddleware = requireBearerAuth({ - verifier: tokenVerifier, requiredScopes: [], - resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl) + resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl), + strictResource: true, + expectedResource: mcpServerUrl }); /** diff --git a/examples/server/src/honoWebStandardStreamableHttp.ts b/examples/server/src/honoWebStandardStreamableHttp.ts index f5c59cffe..aef1e99e2 100644 --- a/examples/server/src/honoWebStandardStreamableHttp.ts +++ b/examples/server/src/honoWebStandardStreamableHttp.ts @@ -10,7 +10,6 @@ import { serve } from '@hono/node-server'; import type { CallToolResult } from '@modelcontextprotocol/server'; import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { mcpStreamableHttpHandler } from '@modelcontextprotocol/server-hono'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import * as z from 'zod/v4'; @@ -57,7 +56,7 @@ app.use( app.get('/health', c => c.json({ status: 'ok' })); // MCP endpoint -app.all('/mcp', mcpStreamableHttpHandler(transport)); +app.all('/mcp', c => transport.handleRequest(c.req.raw)); // Start the server const PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000; diff --git a/examples/server/src/simpleSseServer.ts b/examples/server/src/simpleSseServer.ts deleted file mode 100644 index 35b48b69d..000000000 --- a/examples/server/src/simpleSseServer.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type { CallToolResult } from '@modelcontextprotocol/server'; -import { McpServer, SSEServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; -import type { Request, Response } from 'express'; -import * as z from 'zod/v4'; - -/** - * This example server demonstrates the deprecated HTTP+SSE transport - * (protocol version 2024-11-05). It mainly used for testing backward compatible clients. - * - * The server exposes two endpoints: - * - /mcp: For establishing the SSE stream (GET) - * - /messages: For receiving client messages (POST) - * - */ - -// Create an MCP server instance -const getServer = () => { - const server = new McpServer( - { - name: 'simple-sse-server', - version: '1.0.0' - }, - { capabilities: { logging: {} } } - ); - - server.registerTool( - 'start-notification-stream', - { - description: 'Starts sending periodic notifications', - inputSchema: { - interval: z.number().describe('Interval in milliseconds between notifications').default(1000), - count: z.number().describe('Number of notifications to send').default(10) - } - }, - async ({ interval, count }, extra): Promise => { - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - let counter = 0; - - // Send the initial notification - await server.sendLoggingMessage( - { - level: 'info', - data: `Starting notification stream with ${count} messages every ${interval}ms` - }, - extra.sessionId - ); - - // Send periodic notifications - while (counter < count) { - counter++; - await sleep(interval); - - try { - await server.sendLoggingMessage( - { - level: 'info', - data: `Notification #${counter} at ${new Date().toISOString()}` - }, - extra.sessionId - ); - } catch (error) { - console.error('Error sending notification:', error); - } - } - - return { - content: [ - { - type: 'text', - text: `Completed sending ${count} notifications every ${interval}ms` - } - ] - }; - } - ); - return server; -}; - -const app = createMcpExpressApp(); - -// Store transports by session ID -const transports: Record = {}; - -// SSE endpoint for establishing the stream -app.get('/mcp', async (req: Request, res: Response) => { - console.log('Received GET request to /sse (establishing SSE stream)'); - - try { - // Create a new SSE transport for the client - // The endpoint for POST messages is '/messages' - const transport = new SSEServerTransport('/messages', res); - - // Store the transport by session ID - const sessionId = transport.sessionId; - transports[sessionId] = transport; - - // Set up onclose handler to clean up transport when closed - transport.onclose = () => { - console.log(`SSE transport closed for session ${sessionId}`); - delete transports[sessionId]; - }; - - // Connect the transport to the MCP server - const server = getServer(); - await server.connect(transport); - - console.log(`Established SSE stream with session ID: ${sessionId}`); - } catch (error) { - console.error('Error establishing SSE stream:', error); - if (!res.headersSent) { - res.status(500).send('Error establishing SSE stream'); - } - } -}); - -// Messages endpoint for receiving client JSON-RPC requests -app.post('/messages', async (req: Request, res: Response) => { - console.log('Received POST request to /messages'); - - // Extract session ID from URL query parameter - // In the SSE protocol, this is added by the client based on the endpoint event - const sessionId = req.query.sessionId as string | undefined; - - if (!sessionId) { - console.error('No session ID provided in request URL'); - res.status(400).send('Missing sessionId parameter'); - return; - } - - const transport = transports[sessionId]; - if (!transport) { - console.error(`No active transport found for session ID: ${sessionId}`); - res.status(404).send('Session not found'); - return; - } - - try { - // Handle the POST message with the transport - await transport.handlePostMessage(req, res, req.body); - } catch (error) { - console.error('Error handling request:', error); - if (!res.headersSent) { - res.status(500).send('Error handling request'); - } - } -}); - -// Start the server -const PORT = 3000; -app.listen(PORT, error => { - if (error) { - console.error('Failed to start server:', error); - process.exit(1); - } - console.log(`Simple SSE Server (deprecated protocol version 2024-11-05) listening on port ${PORT}`); -}); - -// Handle server shutdown -process.on('SIGINT', async () => { - console.log('Shutting down server...'); - - // Close all active transports to properly clean up resources - for (const sessionId in transports) { - try { - console.log(`Closing transport for session ${sessionId}`); - await transports[sessionId]!.close(); - delete transports[sessionId]; - } catch (error) { - console.error(`Error closing transport for session ${sessionId}:`, error); - } - } - console.log('Server shutdown complete'); - process.exit(0); -}); diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index c1656a544..b90820266 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -1,6 +1,11 @@ import { randomUUID } from 'node:crypto'; -import { setupAuthServer } from '@modelcontextprotocol/examples-shared'; +import { + getOAuthProtectedResourceMetadataUrl, + mcpAuthMetadataRouter, + requireBearerAuth, + setupAuthServer +} from '@modelcontextprotocol/examples-shared'; import type { CallToolResult, GetPromptResult, @@ -10,16 +15,14 @@ import type { ResourceLink } from '@modelcontextprotocol/server'; import { - checkResourceAllowed, ElicitResultSchema, - getOAuthProtectedResourceMetadataUrl, InMemoryTaskMessageQueue, InMemoryTaskStore, isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp, mcpAuthMetadataRouter, requireBearerAuth } from '@modelcontextprotocol/server-express'; +import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; @@ -527,49 +530,6 @@ if (useOAuth) { const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth }); - const tokenVerifier = { - verifyAccessToken: async (token: string) => { - const endpoint = oauthMetadata.introspection_endpoint; - - if (!endpoint) { - throw new Error('No token verification endpoint available in metadata'); - } - - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - token: token - }).toString() - }); - - if (!response.ok) { - const text = await response.text().catch(() => null); - throw new Error(`Invalid or expired token: ${text}`); - } - - const data = (await response.json()) as { aud: string; client_id: string; scope: string; exp: number }; - - if (strictOAuth) { - if (!data.aud) { - throw new Error(`Resource Indicator (RFC8707) missing`); - } - if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) { - throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`); - } - } - - // Convert the response to AuthInfo format - return { - token, - clientId: data.client_id, - scopes: data.scope ? data.scope.split(' ') : [], - expiresAt: data.exp - }; - } - }; // Add metadata routes to the main MCP server app.use( mcpAuthMetadataRouter({ @@ -581,9 +541,10 @@ if (useOAuth) { ); authMiddleware = requireBearerAuth({ - verifier: tokenVerifier, requiredScopes: [], - resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl) + resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl), + strictResource: strictOAuth, + expectedResource: mcpServerUrl }); } @@ -599,8 +560,8 @@ const mcpPostHandler = async (req: Request, res: Response) => { console.log('Request body:', req.body); } - if (useOAuth && req.auth) { - console.log('Authenticated user:', req.auth); + if (useOAuth && req.app.locals.auth) { + console.log('Authenticated user:', req.app.locals.auth); } try { let transport: NodeStreamableHTTPServerTransport; @@ -683,8 +644,8 @@ const mcpGetHandler = async (req: Request, res: Response) => { return; } - if (useOAuth && req.auth) { - console.log('Authenticated SSE connection from user:', req.auth); + if (useOAuth && req.app.locals.auth) { + console.log('Authenticated SSE connection from user:', req.app.locals.auth); } // Check for Last-Event-ID header for resumability diff --git a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts deleted file mode 100644 index bb2636ea3..000000000 --- a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { randomUUID } from 'node:crypto'; - -import type { CallToolResult } from '@modelcontextprotocol/server'; -import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport, SSEServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; -import type { Request, Response } from 'express'; -import * as z from 'zod/v4'; - -import { InMemoryEventStore } from './inMemoryEventStore.js'; - -/** - * This example server demonstrates backwards compatibility with both: - * 1. The deprecated HTTP+SSE transport (protocol version 2024-11-05) - * 2. The Streamable HTTP transport (protocol version 2025-11-25) - * - * It maintains a single MCP server instance but exposes two transport options: - * - /mcp: The new Streamable HTTP endpoint (supports GET/POST/DELETE) - * - /sse: The deprecated SSE endpoint for older clients (GET to establish stream) - * - /messages: The deprecated POST endpoint for older clients (POST to send messages) - */ - -const getServer = () => { - const server = new McpServer( - { - name: 'backwards-compatible-server', - version: '1.0.0' - }, - { capabilities: { logging: {} } } - ); - - // Register a simple tool that sends notifications over time - server.registerTool( - 'start-notification-stream', - { - description: 'Starts sending periodic notifications for testing resumability', - inputSchema: { - interval: z.number().describe('Interval in milliseconds between notifications').default(100), - count: z.number().describe('Number of notifications to send (0 for 100)').default(50) - } - }, - async ({ interval, count }, extra): Promise => { - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - let counter = 0; - - while (count === 0 || counter < count) { - counter++; - try { - await server.sendLoggingMessage( - { - level: 'info', - data: `Periodic notification #${counter} at ${new Date().toISOString()}` - }, - extra.sessionId - ); - } catch (error) { - console.error('Error sending notification:', error); - } - // Wait for the specified interval - await sleep(interval); - } - - return { - content: [ - { - type: 'text', - text: `Started sending periodic notifications every ${interval}ms` - } - ] - }; - } - ); - return server; -}; - -// Create Express application -const app = createMcpExpressApp(); - -// Store transports by session ID -const transports: Record = {}; - -//============================================================================= -// STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-11-25) -//============================================================================= - -// Handle all MCP Streamable HTTP requests (GET, POST, DELETE) on a single endpoint -app.all('/mcp', async (req: Request, res: Response) => { - console.log(`Received ${req.method} request to /mcp`); - - try { - // Check for existing session ID - const sessionId = req.headers['mcp-session-id'] as string | undefined; - let transport: NodeStreamableHTTPServerTransport; - - if (sessionId && transports[sessionId]) { - // Check if the transport is of the correct type - const existingTransport = transports[sessionId]; - if (existingTransport instanceof NodeStreamableHTTPServerTransport) { - // Reuse existing transport - transport = existingTransport; - } else { - // Transport exists but is not a NodeStreamableHTTPServerTransport (could be SSEServerTransport) - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: Session exists but uses a different transport protocol' - }, - id: null - }); - return; - } - } else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) { - const eventStore = new InMemoryEventStore(); - transport = new NodeStreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability - onsessioninitialized: sessionId => { - // Store the transport by session ID when session is initialized - console.log(`StreamableHTTP session initialized with ID: ${sessionId}`); - transports[sessionId] = transport; - } - }); - - // Set up onclose handler to clean up transport when closed - transport.onclose = () => { - const sid = transport.sessionId; - if (sid && transports[sid]) { - console.log(`Transport closed for session ${sid}, removing from transports map`); - delete transports[sid]; - } - }; - - // Connect the transport to the MCP server - const server = getServer(); - await server.connect(transport); - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: No valid session ID provided' - }, - id: null - }); - return; - } - - // Handle the request with the transport - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error('Error handling MCP request:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error' - }, - id: null - }); - } - } -}); - -//============================================================================= -// DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05) -//============================================================================= - -app.get('/sse', async (req: Request, res: Response) => { - console.log('Received GET request to /sse (deprecated SSE transport)'); - const transport = new SSEServerTransport('/messages', res); - transports[transport.sessionId] = transport; - res.on('close', () => { - delete transports[transport.sessionId]; - }); - const server = getServer(); - await server.connect(transport); -}); - -app.post('/messages', async (req: Request, res: Response) => { - const sessionId = req.query.sessionId as string; - let transport: SSEServerTransport; - const existingTransport = transports[sessionId]; - if (existingTransport instanceof SSEServerTransport) { - // Reuse existing transport - transport = existingTransport; - } else { - // Transport exists but is not a SSEServerTransport (could be NodeStreamableHTTPServerTransport) - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: Session exists but uses a different transport protocol' - }, - id: null - }); - return; - } - if (transport) { - await transport.handlePostMessage(req, res, req.body); - } else { - res.status(400).send('No transport found for sessionId'); - } -}); - -// Start the server -const PORT = 3000; -app.listen(PORT, error => { - if (error) { - console.error('Failed to start server:', error); - process.exit(1); - } - console.log(`Backwards compatible MCP server listening on port ${PORT}`); - console.log(` -============================================== -SUPPORTED TRANSPORT OPTIONS: - -1. Streamable Http(Protocol version: 2025-11-25) - Endpoint: /mcp - Methods: GET, POST, DELETE - Usage: - - Initialize with POST to /mcp - - Establish SSE stream with GET to /mcp - - Send requests with POST to /mcp - - Terminate session with DELETE to /mcp - -2. Http + SSE (Protocol version: 2024-11-05) - Endpoints: /sse (GET) and /messages (POST) - Usage: - - Establish SSE stream with GET to /sse - - Send requests with POST to /messages?sessionId= -============================================== -`); -}); - -// Handle server shutdown -process.on('SIGINT', async () => { - console.log('Shutting down server...'); - - // Close all active transports to properly clean up resources - for (const sessionId in transports) { - try { - console.log(`Closing transport for session ${sessionId}`); - await transports[sessionId]!.close(); - delete transports[sessionId]; - } catch (error) { - console.error(`Error closing transport for session ${sessionId}:`, error); - } - } - console.log('Server shutdown complete'); - process.exit(0); -}); diff --git a/examples/shared/package.json b/examples/shared/package.json index 2d0f6ebe9..aad10e3f6 100644 --- a/examples/shared/package.json +++ b/examples/shared/package.json @@ -34,18 +34,22 @@ "client": "tsx scripts/cli.ts client" }, "dependencies": { + "@modelcontextprotocol/core": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/server-express": "workspace:^", + "better-auth": "^1.4.7", + "better-sqlite3": "^11.10.0", "express": "catalog:runtimeServerOnly" }, "devDependencies": { - "@modelcontextprotocol/tsconfig": "workspace:^", + "@eslint/js": "catalog:devTools", "@modelcontextprotocol/eslint-config": "workspace:^", - "@modelcontextprotocol/vitest-config": "workspace:^", "@modelcontextprotocol/test-helpers": "workspace:^", + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@types/better-sqlite3": "^7.6.13", "@types/express": "catalog:devTools", "@typescript/native-preview": "catalog:devTools", - "@eslint/js": "catalog:devTools", "eslint": "catalog:devTools", "eslint-config-prettier": "catalog:devTools", "eslint-plugin-n": "catalog:devTools", diff --git a/examples/shared/src/auth.ts b/examples/shared/src/auth.ts new file mode 100644 index 000000000..8bf9c8c11 --- /dev/null +++ b/examples/shared/src/auth.ts @@ -0,0 +1,82 @@ +/** + * Better Auth configuration for MCP demo servers + * + * DEMO ONLY - NOT FOR PRODUCTION + * + * This configuration uses in-memory SQLite and auto-approves all logins. + * For production use, configure a proper database and authentication flow. + */ + +import type { BetterAuthPlugin } from 'better-auth'; +import { betterAuth } from 'better-auth'; +import { mcp } from 'better-auth/plugins'; +import Database from 'better-sqlite3'; + +// Create the in-memory database once (module-level singleton) +// This avoids the type export issue and ensures the same DB is used +let _db: InstanceType | null = null; + +function getDatabase(): InstanceType { + if (!_db) { + _db = new Database(':memory:'); + } + return _db; +} + +export interface CreateDemoAuthOptions { + baseURL: string; + resource?: string; + loginPage?: string; +} + +/** + * Creates a better-auth instance configured for MCP OAuth demo. + * + * @param options - Configuration options + * @param options.baseURL - The base URL for the auth server (e.g., http://localhost:3001) + * @param options.resource - The MCP resource server URL (for protected resource metadata) + * @param options.loginPage - Path to login page (defaults to /sign-in) + * + * @see https://www.better-auth.com/docs/plugins/mcp + */ +export function createDemoAuth(options: CreateDemoAuthOptions) { + const { baseURL, resource, loginPage = '/sign-in' } = options; + + // Use in-memory SQLite database for demo purposes + // Note: All data is lost on restart - demo only! + const db = getDatabase(); + + // MCP plugin configuration + const mcpPlugin = mcp({ + loginPage, + resource, + oidcConfig: { + loginPage, + codeExpiresIn: 600, // 10 minutes + accessTokenExpiresIn: 3600, // 1 hour + refreshTokenExpiresIn: 604800, // 7 days + defaultScope: 'openid', + scopes: ['openid', 'profile', 'email', 'offline_access', 'mcp:tools'] + } + }); + + return betterAuth({ + baseURL, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + database: db as any, // Type cast to avoid exposing better-sqlite3 in exported types + trustedOrigins: ['*'], + // Basic email+password for demo + emailAndPassword: { + enabled: true, + requireEmailVerification: false + }, + plugins: [mcpPlugin as BetterAuthPlugin] + }); +} + +/** + * Type for the auth instance returned by createDemoAuth. + * Note: Due to plugin type inference complexity, we use a generic type. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type DemoAuth = ReturnType; diff --git a/examples/shared/src/authMiddleware.ts b/examples/shared/src/authMiddleware.ts new file mode 100644 index 000000000..e46d9e410 --- /dev/null +++ b/examples/shared/src/authMiddleware.ts @@ -0,0 +1,125 @@ +/** + * Auth Middleware for MCP Demo Servers + * + * 🚨 DEMO ONLY - NOT FOR PRODUCTION + * + * This provides bearer auth middleware and metadata routes for MCP servers. + */ + +import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/server'; +import type { NextFunction, Request, Response, Router } from 'express'; +import express from 'express'; + +import { verifyAccessToken } from './authServer.js'; + +export interface RequireBearerAuthOptions { + requiredScopes?: string[]; + resourceMetadataUrl?: URL; + strictResource?: boolean; + expectedResource?: URL; +} + +/** + * Express middleware that requires a valid Bearer token. + * Sets `req.app.locals.auth` on success. + */ +export function requireBearerAuth( + options: RequireBearerAuthOptions = {} +): (req: Request, res: Response, next: NextFunction) => Promise { + const { requiredScopes = [], resourceMetadataUrl, strictResource = false, expectedResource } = options; + + return async (req: Request, res: Response, next: NextFunction): Promise => { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + const wwwAuthenticate = resourceMetadataUrl ? `Bearer resource_metadata="${resourceMetadataUrl.toString()}"` : 'Bearer'; + + res.set('WWW-Authenticate', wwwAuthenticate); + res.status(401).json({ + error: 'unauthorized', + error_description: 'Missing or invalid Authorization header' + }); + return; + } + + const token = authHeader.slice(7); // Remove 'Bearer ' prefix + + try { + const authInfo = await verifyAccessToken(token, { + strictResource, + expectedResource + }); + + // Check required scopes + if (requiredScopes.length > 0) { + const hasAllScopes = requiredScopes.every(scope => authInfo.scopes.includes(scope)); + if (!hasAllScopes) { + res.status(403).json({ + error: 'insufficient_scope', + error_description: `Required scopes: ${requiredScopes.join(', ')}` + }); + return; + } + } + + req.app.locals.auth = authInfo; + next(); + } catch (error) { + const wwwAuthenticate = resourceMetadataUrl + ? `Bearer error="invalid_token", resource_metadata="${resourceMetadataUrl.toString()}"` + : 'Bearer error="invalid_token"'; + + res.set('WWW-Authenticate', wwwAuthenticate); + res.status(401).json({ + error: 'invalid_token', + error_description: error instanceof Error ? error.message : 'Invalid token' + }); + } + }; +} + +export interface McpAuthMetadataRouterOptions { + oauthMetadata: OAuthMetadata; + resourceServerUrl: URL; + scopesSupported?: string[]; + resourceName?: string; +} + +/** + * Creates an Express router that serves OAuth and Protected Resource metadata. + */ +export function mcpAuthMetadataRouter(options: McpAuthMetadataRouterOptions): Router { + const { oauthMetadata, resourceServerUrl, scopesSupported = ['mcp:tools'], resourceName } = options; + + const router = express.Router(); + + // OAuth Protected Resource Metadata (RFC 9728) + const protectedResourceMetadata: OAuthProtectedResourceMetadata = { + resource: resourceServerUrl.toString(), + authorization_servers: [oauthMetadata.issuer], + scopes_supported: scopesSupported, + resource_name: resourceName + }; + + // Serve protected resource metadata + router.get('/.well-known/oauth-protected-resource', (req: Request, res: Response) => { + res.json(protectedResourceMetadata); + }); + + // Also serve at the MCP-specific path + const mcpPath = new URL(resourceServerUrl.pathname, resourceServerUrl).pathname; + router.get(`${mcpPath}/.well-known/oauth-protected-resource`, (req: Request, res: Response) => { + res.json(protectedResourceMetadata); + }); + + return router; +} + +/** + * Helper to get the protected resource metadata URL from a server URL. + */ +export function getOAuthProtectedResourceMetadataUrl(serverUrl: URL): URL { + const metadataUrl = new URL(serverUrl); + metadataUrl.pathname = `${serverUrl.pathname}/.well-known/oauth-protected-resource`.replace(/\/+/g, '/'); + return metadataUrl; +} diff --git a/examples/shared/src/authServer.ts b/examples/shared/src/authServer.ts new file mode 100644 index 000000000..9a14e8978 --- /dev/null +++ b/examples/shared/src/authServer.ts @@ -0,0 +1,243 @@ +/** + * Better Auth Server Setup for MCP Demo + * + * DEMO ONLY - NOT FOR PRODUCTION + * + * This creates a standalone OAuth Authorization Server using better-auth + * that MCP clients can use to obtain access tokens. + * + * See: https://www.better-auth.com/docs/plugins/mcp + */ + +import type { OAuthMetadata } from '@modelcontextprotocol/core'; +import { toNodeHandler } from 'better-auth/node'; +import type { Request, Response as ExpressResponse } from 'express'; +import express from 'express'; + +import type { DemoAuth } from './auth.js'; +import { createDemoAuth } from './auth.js'; + +export interface SetupAuthServerOptions { + authServerUrl: URL; + mcpServerUrl: URL; + strictResource?: boolean; +} + +export interface AuthServerResult { + auth: DemoAuth; + oauthMetadata: OAuthMetadata; +} + +// Store auth instance globally so it can be used for token verification +let globalAuth: DemoAuth | null = null; + +/** + * Gets the global auth instance (must call setupAuthServer first) + */ +export function getAuth(): DemoAuth { + if (!globalAuth) { + throw new Error('Auth not initialized. Call setupAuthServer first.'); + } + return globalAuth; +} + +/** + * Sets up and starts the OAuth Authorization Server on a separate port. + * + * @param options - Server configuration + * @returns OAuth metadata for the authorization server + */ +export function setupAuthServer(options: SetupAuthServerOptions): OAuthMetadata { + const { authServerUrl, mcpServerUrl } = options; + + // Create better-auth instance with MCP plugin + const auth = createDemoAuth({ + baseURL: authServerUrl.toString().replace(/\/$/, ''), + resource: mcpServerUrl.toString(), + loginPage: '/sign-in' + }); + + // Store globally for token verification + globalAuth = auth; + + // Create Express app for auth server + const authApp = express(); + authApp.use(express.json()); + authApp.use(express.urlencoded({ extended: true })); + + // Enable CORS for all origins (demo only) + authApp.use((_req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.header('Access-Control-Expose-Headers', 'WWW-Authenticate'); + if (_req.method === 'OPTIONS') { + res.sendStatus(200); + return; + } + next(); + }); + + // Auto-login page that immediately creates a session and redirects + // This simulates a user logging in and approving the OAuth request + authApp.get('/sign-in', async (req: Request, res: ExpressResponse) => { + // Get the OAuth authorization parameters from the query string + const queryParams = new URLSearchParams(req.query as Record); + const redirectUri = queryParams.get('redirect_uri'); + const clientId = queryParams.get('client_id'); + + if (!redirectUri || !clientId) { + res.status(400).send(` + + + Demo Login + +

Demo OAuth Server

+

Missing required OAuth parameters. This page should be accessed via OAuth flow.

+ + + `); + return; + } + + // For demo: auto-approve by redirecting to the authorization endpoint + // with a flag indicating auto-approval + // In better-auth, we need to create a session first, then complete authorization + + // Set a demo session cookie + const authCookieData = { + userId: 'demo_user', + name: 'Demo User', + timestamp: Date.now() + }; + const cookieValue = encodeURIComponent(JSON.stringify(authCookieData)); + res.cookie('demo_session', cookieValue, { + httpOnly: true, + sameSite: 'lax', + maxAge: 24 * 60 * 60 * 1000 // 24 hours + }); + + // Redirect to the actual authorization handler with auto-approve + // Better-auth handles the OAuth flow at /api/auth/authorize + const authorizeUrl = new URL('/api/auth/authorize', authServerUrl); + authorizeUrl.search = queryParams.toString(); + // Add a flag to indicate auto-approval (this would be handled by a custom flow) + authorizeUrl.searchParams.set('auto_approve', 'true'); + + console.log(`[Auth Server] Auto-approved login for client ${clientId}`); + res.redirect(authorizeUrl.toString()); + }); + + // Mount better-auth handler for all /api/auth/* routes + // This handles: authorization, token, client registration, etc. + authApp.all('/api/auth/*', toNodeHandler(auth)); + + // OAuth metadata endpoints at well-known paths + // Some clients may not parse WWW-Authenticate header and need these + authApp.get('/.well-known/oauth-authorization-server', (_req, res) => { + res.json(createOAuthMetadata(authServerUrl)); + }); + + authApp.get('/.well-known/oauth-protected-resource', (_req, res) => { + res.json({ + resource: mcpServerUrl.toString(), + authorization_servers: [authServerUrl.toString().replace(/\/$/, '')], + scopes_supported: ['openid', 'profile', 'email', 'mcp:tools'] + }); + }); + + // Start the auth server + const authPort = parseInt(authServerUrl.port, 10); + authApp.listen(authPort, (error?: Error) => { + if (error) { + console.error('Failed to start auth server:', error); + process.exit(1); + } + console.log(`OAuth Authorization Server listening on port ${authPort}`); + console.log(` Authorization: ${authServerUrl}api/auth/authorize`); + console.log(` Token: ${authServerUrl}api/auth/token`); + console.log(` Metadata: ${authServerUrl}.well-known/oauth-authorization-server`); + }); + + return createOAuthMetadata(authServerUrl); +} + +/** + * Creates OAuth 2.0 Authorization Server Metadata (RFC 8414) + */ +function createOAuthMetadata(issuerUrl: URL): OAuthMetadata { + const issuer = issuerUrl.toString().replace(/\/$/, ''); + const apiAuthBase = `${issuer}/api/auth`; + + return { + issuer, + authorization_endpoint: `${apiAuthBase}/authorize`, + token_endpoint: `${apiAuthBase}/token`, + registration_endpoint: `${apiAuthBase}/register`, + introspection_endpoint: `${apiAuthBase}/introspect`, + scopes_supported: ['openid', 'profile', 'email', 'offline_access', 'mcp:tools'], + response_types_supported: ['code'], + grant_types_supported: ['authorization_code', 'refresh_token'], + token_endpoint_auth_methods_supported: ['none', 'client_secret_post', 'client_secret_basic'], + code_challenge_methods_supported: ['S256'] + }; +} + +/** + * Verifies an access token using better-auth's getMcpSession. + * This can be used by MCP servers to validate tokens. + */ +export async function verifyAccessToken( + token: string, + options?: { strictResource?: boolean; expectedResource?: URL } +): Promise<{ + token: string; + clientId: string; + scopes: string[]; + expiresAt: number; +}> { + const auth = getAuth(); + + try { + // Create a mock request with the Authorization header + const headers = new Headers(); + headers.set('Authorization', `Bearer ${token}`); + + // Use better-auth's getMcpSession API + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session = await (auth.api as any).getMcpSession({ + headers + }); + + if (!session) { + throw new Error('Invalid token'); + } + + // OAuthAccessToken has: + // - accessToken, refreshToken: string + // - accessTokenExpiresAt, refreshTokenExpiresAt: Date + // - clientId, userId: string + // - scopes: string (space-separated) + const scopes = typeof session.scopes === 'string' ? session.scopes.split(' ') : ['openid']; + const expiresAt = session.accessTokenExpiresAt + ? Math.floor(new Date(session.accessTokenExpiresAt).getTime() / 1000) + : Math.floor(Date.now() / 1000) + 3600; + + // Note: better-auth's OAuthAccessToken doesn't have a resource field + // Resource validation would need to be done at a different layer + if (options?.strictResource && options.expectedResource) { + // For now, we skip resource validation as it's not in the session + // In production, you'd store and validate this separately + console.warn('[Auth] Resource validation requested but not available in better-auth session'); + } + + return { + token, + clientId: session.clientId, + scopes, + expiresAt + }; + } catch (error) { + throw new Error(`Token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} diff --git a/examples/shared/src/demoInMemoryOAuthProvider.ts b/examples/shared/src/demoInMemoryOAuthProvider.ts deleted file mode 100644 index 23b168224..000000000 --- a/examples/shared/src/demoInMemoryOAuthProvider.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { randomUUID } from 'node:crypto'; - -import type { - AuthInfo, - AuthorizationParams, - OAuthClientInformationFull, - OAuthMetadata, - OAuthRegisteredClientsStore, - OAuthServerProvider, - OAuthTokens -} from '@modelcontextprotocol/server'; -import { createOAuthMetadata, InvalidRequestError, resourceUrlFromServerUrl } from '@modelcontextprotocol/server'; -import { mcpAuthRouter } from '@modelcontextprotocol/server-express'; -import type { Request, Response as ExpressResponse } from 'express'; -import express from 'express'; - -export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore { - private clients = new Map(); - - async getClient(clientId: string) { - return this.clients.get(clientId); - } - - async registerClient(clientMetadata: OAuthClientInformationFull) { - this.clients.set(clientMetadata.client_id, clientMetadata); - return clientMetadata; - } -} - -/** - * 🚨 DEMO ONLY - NOT FOR PRODUCTION - * - * This example demonstrates MCP OAuth flow but lacks some of the features required for production use, - * for example: - * - Persistent token storage - * - Rate limiting - */ -export class DemoInMemoryAuthProvider implements OAuthServerProvider { - clientsStore = new DemoInMemoryClientsStore(); - private codes = new Map< - string, - { - params: AuthorizationParams; - client: OAuthClientInformationFull; - } - >(); - private tokens = new Map(); - - constructor(private validateResource?: (resource?: URL) => boolean) {} - - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams): Promise { - const code = randomUUID(); - - const searchParams = new URLSearchParams({ - code - }); - if (params.state !== undefined) { - searchParams.set('state', params.state); - } - - this.codes.set(code, { - client, - params - }); - - // Simulate a user login - // Set a secure HTTP-only session cookie with authorization info - const authCookieData = { - userId: 'demo_user', - name: 'Demo User', - timestamp: Date.now() - }; - const cookieValue = encodeURIComponent(JSON.stringify(authCookieData)); - const maxAgeSeconds = 24 * 60 * 60; // 24 hours - demo only - const setCookie = `demo_session=${cookieValue}; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSeconds}; Path=/`; - - if (!client.redirect_uris.includes(params.redirectUri)) { - throw new InvalidRequestError('Unregistered redirect_uri'); - } - const targetUrl = new URL(params.redirectUri); - targetUrl.search = searchParams.toString(); - const redirectResponse = Response.redirect(targetUrl.toString(), 302); - const headers = new Headers(redirectResponse.headers); - headers.append('Set-Cookie', setCookie); - return new Response(null, { status: redirectResponse.status, headers }); - } - - async challengeForAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise { - // Store the challenge with the code data - const codeData = this.codes.get(authorizationCode); - if (!codeData) { - throw new Error('Invalid authorization code'); - } - - return codeData.params.codeChallenge; - } - - async exchangeAuthorizationCode( - client: OAuthClientInformationFull, - authorizationCode: string, - // Note: code verifier is checked in token.ts by default - // it's unused here for that reason. - _codeVerifier?: string - ): Promise { - const codeData = this.codes.get(authorizationCode); - if (!codeData) { - throw new Error('Invalid authorization code'); - } - - if (codeData.client.client_id !== client.client_id) { - throw new Error(`Authorization code was not issued to this client, ${codeData.client.client_id} != ${client.client_id}`); - } - - if (this.validateResource && !this.validateResource(codeData.params.resource)) { - throw new Error(`Invalid resource: ${codeData.params.resource}`); - } - - this.codes.delete(authorizationCode); - const token = randomUUID(); - - const tokenData = { - token, - clientId: client.client_id, - scopes: codeData.params.scopes || [], - expiresAt: Date.now() + 3600000, // 1 hour - resource: codeData.params.resource, - type: 'access' - }; - - this.tokens.set(token, tokenData); - - return { - access_token: token, - token_type: 'bearer', - expires_in: 3600, - scope: (codeData.params.scopes || []).join(' ') - }; - } - - async exchangeRefreshToken( - _client: OAuthClientInformationFull, - _refreshToken: string, - _scopes?: string[], - _resource?: URL - ): Promise { - throw new Error('Not implemented for example demo'); - } - - async verifyAccessToken(token: string): Promise { - const tokenData = this.tokens.get(token); - if (!tokenData || !tokenData.expiresAt || tokenData.expiresAt < Date.now()) { - throw new Error('Invalid or expired token'); - } - - return { - token, - clientId: tokenData.clientId, - scopes: tokenData.scopes, - expiresAt: Math.floor(tokenData.expiresAt / 1000), - resource: tokenData.resource - }; - } -} - -export const setupAuthServer = ({ - authServerUrl, - mcpServerUrl, - strictResource -}: { - authServerUrl: URL; - mcpServerUrl: URL; - strictResource: boolean; -}): OAuthMetadata => { - // Create separate auth server app - // NOTE: This is a separate app on a separate port to illustrate - // how to separate an OAuth Authorization Server from a Resource - // server in the SDK. The SDK is not intended to be provide a standalone - // authorization server. - - const validateResource = strictResource - ? (resource?: URL) => { - if (!resource) return false; - const expectedResource = resourceUrlFromServerUrl(mcpServerUrl); - return resource.toString() === expectedResource.toString(); - } - : undefined; - - const provider = new DemoInMemoryAuthProvider(validateResource); - const authApp = express(); - authApp.use(express.json()); - // For introspection requests - authApp.use(express.urlencoded()); - - // Add OAuth routes to the auth server - // NOTE: this will also add a protected resource metadata route, - // but it won't be used, so leave it. - authApp.use( - mcpAuthRouter({ - provider, - issuerUrl: authServerUrl, - scopesSupported: ['mcp:tools'] - }) - ); - - authApp.post('/introspect', async (req: Request, res: ExpressResponse) => { - try { - const { token } = req.body; - if (!token) { - res.status(400).json({ error: 'Token is required' }); - return; - } - - const tokenInfo = await provider.verifyAccessToken(token); - res.json({ - active: true, - client_id: tokenInfo.clientId, - scope: tokenInfo.scopes.join(' '), - exp: tokenInfo.expiresAt, - aud: tokenInfo.resource - }); - return; - } catch (error) { - res.status(401).json({ - active: false, - error: 'Unauthorized', - error_description: `Invalid token: ${error}` - }); - } - }); - - const auth_port = authServerUrl.port; - // Start the auth server - authApp.listen(auth_port, error => { - if (error) { - console.error('Failed to start server:', error); - process.exit(1); - } - console.log(`OAuth Authorization Server listening on port ${auth_port}`); - }); - - // Note: we could fetch this from the server, but then we end up - // with some top level async which gets annoying. - const oauthMetadata: OAuthMetadata = createOAuthMetadata({ - provider, - issuerUrl: authServerUrl, - scopesSupported: ['mcp:tools'] - }); - - oauthMetadata.introspection_endpoint = new URL('/introspect', authServerUrl).href; - - return oauthMetadata; -}; diff --git a/examples/shared/src/index.ts b/examples/shared/src/index.ts index 1c31cf06e..21d189bbc 100644 --- a/examples/shared/src/index.ts +++ b/examples/shared/src/index.ts @@ -1 +1,11 @@ -export * from './demoInMemoryOAuthProvider.js'; +// Auth configuration +export type { CreateDemoAuthOptions, DemoAuth } from './auth.js'; +export { createDemoAuth } from './auth.js'; + +// Auth middleware +export type { McpAuthMetadataRouterOptions, RequireBearerAuthOptions } from './authMiddleware.js'; +export { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, requireBearerAuth } from './authMiddleware.js'; + +// Auth server setup +export type { AuthServerResult, SetupAuthServerOptions } from './authServer.js'; +export { getAuth, setupAuthServer, verifyAccessToken } from './authServer.js'; diff --git a/examples/shared/test/demoInMemoryOAuthProvider.test.ts b/examples/shared/test/demoInMemoryOAuthProvider.test.ts index 0c1c887aa..5ea1fea9c 100644 --- a/examples/shared/test/demoInMemoryOAuthProvider.test.ts +++ b/examples/shared/test/demoInMemoryOAuthProvider.test.ts @@ -1,54 +1,35 @@ -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import type { AuthorizationParams } from '@modelcontextprotocol/server'; -import { InvalidRequestError } from '@modelcontextprotocol/server'; -import { beforeEach, describe, expect, it } from 'vitest'; - -import { DemoInMemoryAuthProvider } from '../src/demoInMemoryOAuthProvider.js'; - -describe('DemoInMemoryAuthProvider', () => { - let provider: DemoInMemoryAuthProvider; - - beforeEach(() => { - provider = new DemoInMemoryAuthProvider(); +/** + * Tests for the demo OAuth provider using better-auth + * + * DEMO ONLY - NOT FOR PRODUCTION + * + * The demo OAuth provider now uses better-auth with the MCP plugin. + * These tests verify the basic setup works correctly. + */ + +import { describe, expect, it } from 'vitest'; + +import type {CreateDemoAuthOptions} from '../src/auth.js'; +import { createDemoAuth } from '../src/auth.js'; + +describe('createDemoAuth', () => { + const validOptions: CreateDemoAuthOptions = { + baseURL: 'http://localhost:3001', + resource: 'http://localhost:3000/mcp', + loginPage: '/sign-in' + }; + + it('creates a better-auth instance with MCP plugin', () => { + const auth = createDemoAuth(validOptions); + expect(auth).toBeDefined(); + expect(auth.api).toBeDefined(); }); - describe('authorize', () => { - const validClient: OAuthClientInformationFull = { - client_id: 'test-client', - client_secret: 'test-secret', - redirect_uris: ['https://example.com/callback', 'https://example.com/callback2'], - scope: 'test-scope' + it('uses default loginPage when not specified', () => { + const options: CreateDemoAuthOptions = { + baseURL: 'http://localhost:3001' }; - - it('redirects to redirect_uri when valid', async () => { - const params: AuthorizationParams = { - redirectUri: 'https://example.com/callback', - state: 'test-state', - codeChallenge: 'test-challenge', - scopes: ['test-scope'] - }; - - const res = await provider.authorize(validClient, params); - expect(res.status).toBe(302); - const location = res.headers.get('location'); - expect(location).toBeTruthy(); - const url = new URL(location!); - expect(url.origin + url.pathname).toBe('https://example.com/callback'); - expect(url.searchParams.get('state')).toBe('test-state'); - expect(url.searchParams.get('code')).toBeTruthy(); - expect(res.headers.get('set-cookie')).toContain('demo_session='); - }); - - it('throws InvalidRequestError for unregistered redirect_uri', async () => { - const params: AuthorizationParams = { - redirectUri: 'https://evil.com/callback', - state: 'test-state', - codeChallenge: 'test-challenge', - scopes: ['test-scope'] - }; - - await expect(provider.authorize(validClient, params)).rejects.toThrow(InvalidRequestError); - await expect(provider.authorize(validClient, params)).rejects.toThrow('Unregistered redirect_uri'); - }); + const auth = createDemoAuth(options); + expect(auth).toBeDefined(); }); }); diff --git a/packages/server-express/src/auth/bearerAuth.ts b/packages/server-express/src/auth/bearerAuth.ts deleted file mode 100644 index d8d0aad8b..000000000 --- a/packages/server-express/src/auth/bearerAuth.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { URL } from 'node:url'; - -import type { AuthInfo } from '@modelcontextprotocol/core'; -import type { BearerAuthMiddlewareOptions } from '@modelcontextprotocol/server'; -import { requireBearerAuth as requireBearerAuthWeb } from '@modelcontextprotocol/server'; -import { sendResponse } from '@remix-run/node-fetch-server'; -import type { NextFunction, Request as ExpressRequest, RequestHandler } from 'express'; - -declare module 'express-serve-static-core' { - interface Request { - /** - * Information about the validated access token, if `requireBearerAuth` was used. - */ - auth?: AuthInfo; - } -} - -function expressRequestUrl(req: ExpressRequest): URL { - const host = req.get('host') ?? req.headers.host ?? 'localhost'; - const protocol = req.protocol ?? 'http'; - const path = req.originalUrl ?? req.url ?? '/'; - return new URL(path, `${protocol}://${host}`); -} - -/** - * Express middleware wrapper for the Web-standard `requireBearerAuth` helper. - * - * On success, sets `req.auth` and calls `next()`. - * On failure, writes the JSON error response and ends the request. - */ -export function requireBearerAuth(options: BearerAuthMiddlewareOptions): RequestHandler { - return async (req, res, next: NextFunction) => { - try { - const url = expressRequestUrl(req); - const webReq = new Request(url, { - method: req.method, - headers: { - authorization: req.headers.authorization ?? '' - } - }); - - const result = await requireBearerAuthWeb(webReq, options); - if ('authInfo' in result) { - req.auth = result.authInfo; - next(); - return; - } - - await sendResponse(res, result.response); - } catch (err) { - next(err); - } - }; -} diff --git a/packages/server-express/src/auth/router.ts b/packages/server-express/src/auth/router.ts deleted file mode 100644 index 868149efd..000000000 --- a/packages/server-express/src/auth/router.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { AuthMetadataOptions, AuthRouterOptions } from '@modelcontextprotocol/server'; -import { - getParsedBody, - mcpAuthMetadataRouter as createWebAuthMetadataRouter, - mcpAuthRouter as createWebAuthRouter, - TooManyRequestsError -} from '@modelcontextprotocol/server'; -import { createRequest, sendResponse } from '@remix-run/node-fetch-server'; -import type { RequestHandler } from 'express'; -import express from 'express'; -import { rateLimit } from 'express-rate-limit'; - -export type ExpressAuthRateLimitOptions = - | false - | { - /** - * Window size in ms (default: 60s) - */ - windowMs?: number; - /** - * Max requests per window per client (default: 60) - */ - max?: number; - }; - -/** - * Express router adapter for the Web-standard `mcpAuthRouter` from `@modelcontextprotocol/server`. - * - * IMPORTANT: This router MUST be mounted at the application root, like: - * - * ```ts - * app.use(mcpAuthRouter(...)) - * ``` - */ -export function mcpAuthRouter(options: AuthRouterOptions & { rateLimit?: ExpressAuthRateLimitOptions }): RequestHandler { - const web = createWebAuthRouter(options); - const router = express.Router(); - - const rateLimitOptions = options.rateLimit; - const limiter = - rateLimitOptions === false - ? undefined - : rateLimit({ - windowMs: rateLimitOptions?.windowMs ?? 60_000, - max: rateLimitOptions?.max ?? 60, - standardHeaders: true, - legacyHeaders: false, - handler: (_req, res) => { - const err = new TooManyRequestsError('Too many requests'); - res.status(429).json(err.toResponseObject()); - } - }); - - const isRateLimitedPath = (path: string): boolean => - path === '/authorize' || path === '/token' || path === '/register' || path === '/revoke'; - - for (const route of web.routes) { - const handlers: RequestHandler[] = []; - if (limiter && isRateLimitedPath(route.path)) { - handlers.push(limiter); - } - handlers.push(async (req, res, next) => { - try { - const webReq = createRequest(req, res); - const parsedBody = req.body !== undefined ? req.body : await getParsedBody(webReq); - const webRes = await route.handler(webReq, { parsedBody }); - await sendResponse(res, webRes); - } catch (err) { - next(err); - } - }); - router.all(route.path, ...handlers); - } - - return router; -} - -/** - * Express router adapter for the Web-standard `mcpAuthMetadataRouter` from `@modelcontextprotocol/server`. - * - * IMPORTANT: This router MUST be mounted at the application root. - */ -export function mcpAuthMetadataRouter(options: AuthMetadataOptions): RequestHandler { - const web = createWebAuthMetadataRouter(options); - const router = express.Router(); - - for (const route of web.routes) { - router.all(route.path, async (req, res, next) => { - try { - const webReq = createRequest(req, res); - const parsedBody = req.body !== undefined ? req.body : await getParsedBody(webReq); - const webRes = await route.handler(webReq, { parsedBody }); - await sendResponse(res, webRes); - } catch (err) { - next(err); - } - }); - } - - return router; -} diff --git a/packages/server-express/src/index.ts b/packages/server-express/src/index.ts index 3c5b72fe7..2d7d20a64 100644 --- a/packages/server-express/src/index.ts +++ b/packages/server-express/src/index.ts @@ -1,4 +1,2 @@ -export * from './auth/bearerAuth.js'; -export * from './auth/router.js'; export * from './express.js'; export * from './middleware/hostHeaderValidation.js'; diff --git a/packages/server-express/test/server-express.test.ts b/packages/server-express/test/server-express.test.ts new file mode 100644 index 000000000..9bed5903a --- /dev/null +++ b/packages/server-express/test/server-express.test.ts @@ -0,0 +1,182 @@ +import type { NextFunction, Request, Response } from 'express'; +import { vi } from 'vitest'; + +import { createMcpExpressApp } from '../src/express.js'; +import { hostHeaderValidation, localhostHostValidation } from '../src/middleware/hostHeaderValidation.js'; + +// Helper to create mock Express request/response/next +function createMockReqResNext(host?: string) { + const req = { + headers: { + host + } + } as Request; + + const res = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis() + } as unknown as Response; + + const next = vi.fn() as NextFunction; + + return { req, res, next }; +} + +describe('@modelcontextprotocol/server-express', () => { + describe('hostHeaderValidation', () => { + test('should block invalid Host header', () => { + const middleware = hostHeaderValidation(['localhost']); + const { req, res, next } = createMockReqResNext('evil.com:3000'); + + middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + jsonrpc: '2.0', + error: expect.objectContaining({ + code: -32000 + }), + id: null + }) + ); + expect(next).not.toHaveBeenCalled(); + }); + + test('should allow valid Host header', () => { + const middleware = hostHeaderValidation(['localhost']); + const { req, res, next } = createMockReqResNext('localhost:3000'); + + middleware(req, res, next); + + expect(res.status).not.toHaveBeenCalled(); + expect(res.json).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); + + test('should handle multiple allowed hostnames', () => { + const middleware = hostHeaderValidation(['localhost', '127.0.0.1', 'myapp.local']); + const { req: req1, res: res1, next: next1 } = createMockReqResNext('127.0.0.1:8080'); + const { req: req2, res: res2, next: next2 } = createMockReqResNext('myapp.local'); + + middleware(req1, res1, next1); + middleware(req2, res2, next2); + + expect(next1).toHaveBeenCalled(); + expect(next2).toHaveBeenCalled(); + }); + }); + + describe('localhostHostValidation', () => { + test('should allow localhost', () => { + const middleware = localhostHostValidation(); + const { req, res, next } = createMockReqResNext('localhost:3000'); + + middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('should allow 127.0.0.1', () => { + const middleware = localhostHostValidation(); + const { req, res, next } = createMockReqResNext('127.0.0.1:3000'); + + middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('should allow [::1] (IPv6 localhost)', () => { + const middleware = localhostHostValidation(); + const { req, res, next } = createMockReqResNext('[::1]:3000'); + + middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + test('should block non-localhost hosts', () => { + const middleware = localhostHostValidation(); + const { req, res, next } = createMockReqResNext('evil.com:3000'); + + middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(next).not.toHaveBeenCalled(); + }); + }); + + describe('createMcpExpressApp', () => { + test('should enable localhost DNS rebinding protection by default', () => { + const app = createMcpExpressApp(); + + // The app should be a valid Express application + expect(app).toBeDefined(); + expect(typeof app.use).toBe('function'); + expect(typeof app.get).toBe('function'); + expect(typeof app.post).toBe('function'); + }); + + test('should apply DNS rebinding protection for localhost host', () => { + const app = createMcpExpressApp({ host: 'localhost' }); + expect(app).toBeDefined(); + }); + + test('should apply DNS rebinding protection for ::1 host', () => { + const app = createMcpExpressApp({ host: '::1' }); + expect(app).toBeDefined(); + }); + + test('should use allowedHosts when provided', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); + warn.mockRestore(); + + expect(app).toBeDefined(); + }); + + test('should warn when binding to 0.0.0.0 without allowedHosts', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + createMcpExpressApp({ host: '0.0.0.0' }); + + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('Warning: Server is binding to 0.0.0.0 without DNS rebinding protection') + ); + + warn.mockRestore(); + }); + + test('should warn when binding to :: without allowedHosts', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + createMcpExpressApp({ host: '::' }); + + expect(warn).toHaveBeenCalledWith(expect.stringContaining('Warning: Server is binding to :: without DNS rebinding protection')); + + warn.mockRestore(); + }); + + test('should not warn for 0.0.0.0 when allowedHosts is provided', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); + + expect(warn).not.toHaveBeenCalled(); + + warn.mockRestore(); + }); + + test('should not apply host validation for non-localhost hosts without allowedHosts', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + // For arbitrary hosts (not 0.0.0.0 or ::), no validation is applied and no warning + const app = createMcpExpressApp({ host: '192.168.1.1' }); + + expect(warn).not.toHaveBeenCalled(); + expect(app).toBeDefined(); + + warn.mockRestore(); + }); + }); +}); diff --git a/packages/server-express/test/server/auth/router.test.ts b/packages/server-express/test/server/auth/router.test.ts deleted file mode 100644 index 9d7638543..000000000 --- a/packages/server-express/test/server/auth/router.test.ts +++ /dev/null @@ -1,500 +0,0 @@ -import type { - AuthInfo, - OAuthClientInformationFull, - OAuthMetadata, - OAuthTokenRevocationRequest, - OAuthTokens -} from '@modelcontextprotocol/server'; -import { InvalidTokenError } from '@modelcontextprotocol/server'; -import express from 'express'; -import supertest from 'supertest'; - -import type { OAuthRegisteredClientsStore } from '@modelcontextprotocol/server'; -import type { AuthorizationParams, OAuthServerProvider } from '@modelcontextprotocol/server'; -import type { AuthMetadataOptions, AuthRouterOptions } from '@modelcontextprotocol/server'; -import { mcpAuthMetadataRouter, mcpAuthRouter } from '../../../src/auth/router.js'; - -describe('MCP Auth Router', () => { - // Setup mock provider with full capabilities - const mockClientStore: OAuthRegisteredClientsStore = { - async getClient(clientId: string): Promise { - if (clientId === 'valid-client') { - return { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - } - return undefined; - }, - - async registerClient(client: OAuthClientInformationFull): Promise { - return client; - } - }; - - const mockProvider: OAuthServerProvider = { - clientsStore: mockClientStore, - - async authorize(_client: OAuthClientInformationFull, params: AuthorizationParams): Promise { - const redirectUrl = new URL(params.redirectUri); - redirectUrl.searchParams.set('code', 'mock_auth_code'); - if (params.state) { - redirectUrl.searchParams.set('state', params.state); - } - return Response.redirect(redirectUrl.toString(), 302); - }, - - async challengeForAuthorizationCode(): Promise { - return 'mock_challenge'; - }, - - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - - async verifyAccessToken(token: string): Promise { - if (token === 'valid_token') { - return { - token, - clientId: 'valid-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 - }; - } - throw new InvalidTokenError('Token is invalid or expired'); - }, - - async revokeToken(_client: OAuthClientInformationFull, _request: OAuthTokenRevocationRequest): Promise { - // Success - do nothing in mock - } - }; - - // Provider without registration and revocation - const mockProviderMinimal: OAuthServerProvider = { - clientsStore: { - async getClient(clientId: string): Promise { - if (clientId === 'valid-client') { - return { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - } - return undefined; - } - }, - - async authorize(_client: OAuthClientInformationFull, params: AuthorizationParams): Promise { - const redirectUrl = new URL(params.redirectUri); - redirectUrl.searchParams.set('code', 'mock_auth_code'); - if (params.state) { - redirectUrl.searchParams.set('state', params.state); - } - return Response.redirect(redirectUrl.toString(), 302); - }, - - async challengeForAuthorizationCode(): Promise { - return 'mock_challenge'; - }, - - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - - async verifyAccessToken(token: string): Promise { - if (token === 'valid_token') { - return { - token, - clientId: 'valid-client', - scopes: ['read'], - expiresAt: Date.now() / 1000 + 3600 - }; - } - throw new InvalidTokenError('Token is invalid or expired'); - } - }; - - describe('Router creation', () => { - it('throws error for non-HTTPS issuer URL', () => { - const options: AuthRouterOptions = { - provider: mockProvider, - issuerUrl: new URL('http://auth.example.com') - }; - - expect(() => mcpAuthRouter(options)).toThrow('Issuer URL must be HTTPS'); - }); - - it('allows localhost HTTP for development', () => { - const options: AuthRouterOptions = { - provider: mockProvider, - issuerUrl: new URL('http://localhost:3000') - }; - - expect(() => mcpAuthRouter(options)).not.toThrow(); - }); - - it('throws error for issuer URL with fragment', () => { - const options: AuthRouterOptions = { - provider: mockProvider, - issuerUrl: new URL('https://auth.example.com#fragment') - }; - - expect(() => mcpAuthRouter(options)).toThrow('Issuer URL must not have a fragment'); - }); - - it('throws error for issuer URL with query string', () => { - const options: AuthRouterOptions = { - provider: mockProvider, - issuerUrl: new URL('https://auth.example.com?param=value') - }; - - expect(() => mcpAuthRouter(options)).toThrow('Issuer URL must not have a query string'); - }); - - it('successfully creates router with valid options', () => { - const options: AuthRouterOptions = { - provider: mockProvider, - issuerUrl: new URL('https://auth.example.com') - }; - - expect(() => mcpAuthRouter(options)).not.toThrow(); - }); - }); - - describe('Metadata endpoint', () => { - let app: express.Express; - - beforeEach(() => { - // Setup full-featured router - app = express(); - const options: AuthRouterOptions = { - provider: mockProvider, - issuerUrl: new URL('https://auth.example.com'), - serviceDocumentationUrl: new URL('https://docs.example.com') - }; - app.use(mcpAuthRouter(options)); - }); - - it('returns complete metadata for full-featured router', async () => { - const response = await supertest(app).get('/.well-known/oauth-authorization-server'); - - expect(response.status).toBe(200); - - // Verify essential fields - expect(response.body.issuer).toBe('https://auth.example.com/'); - expect(response.body.authorization_endpoint).toBe('https://auth.example.com/authorize'); - expect(response.body.token_endpoint).toBe('https://auth.example.com/token'); - expect(response.body.registration_endpoint).toBe('https://auth.example.com/register'); - expect(response.body.revocation_endpoint).toBe('https://auth.example.com/revoke'); - - // Verify supported features - expect(response.body.response_types_supported).toEqual(['code']); - expect(response.body.grant_types_supported).toEqual(['authorization_code', 'refresh_token']); - expect(response.body.code_challenge_methods_supported).toEqual(['S256']); - expect(response.body.token_endpoint_auth_methods_supported).toEqual(['client_secret_post', 'none']); - expect(response.body.revocation_endpoint_auth_methods_supported).toEqual(['client_secret_post']); - - // Verify optional fields - expect(response.body.service_documentation).toBe('https://docs.example.com/'); - }); - - it('returns minimal metadata for minimal router', async () => { - // Setup minimal router - const minimalApp = express(); - const options: AuthRouterOptions = { - provider: mockProviderMinimal, - issuerUrl: new URL('https://auth.example.com') - }; - minimalApp.use(mcpAuthRouter(options)); - - const response = await supertest(minimalApp).get('/.well-known/oauth-authorization-server'); - - expect(response.status).toBe(200); - - // Verify essential endpoints - expect(response.body.issuer).toBe('https://auth.example.com/'); - expect(response.body.authorization_endpoint).toBe('https://auth.example.com/authorize'); - expect(response.body.token_endpoint).toBe('https://auth.example.com/token'); - - // Verify missing optional endpoints - expect(response.body.registration_endpoint).toBeUndefined(); - expect(response.body.revocation_endpoint).toBeUndefined(); - expect(response.body.revocation_endpoint_auth_methods_supported).toBeUndefined(); - expect(response.body.service_documentation).toBeUndefined(); - }); - - it('provides protected resource metadata', async () => { - // Setup router with draft protocol version - const draftApp = express(); - const options: AuthRouterOptions = { - provider: mockProvider, - issuerUrl: new URL('https://mcp.example.com'), - scopesSupported: ['read', 'write'], - resourceName: 'Test API' - }; - draftApp.use(mcpAuthRouter(options)); - - const response = await supertest(draftApp).get('/.well-known/oauth-protected-resource'); - - expect(response.status).toBe(200); - - // Verify protected resource metadata - expect(response.body.resource).toBe('https://mcp.example.com/'); - expect(response.body.authorization_servers).toContain('https://mcp.example.com/'); - expect(response.body.scopes_supported).toEqual(['read', 'write']); - expect(response.body.resource_name).toBe('Test API'); - }); - }); - - describe('Endpoint routing', () => { - let app: express.Express; - - beforeEach(() => { - // Setup full-featured router - app = express(); - const options: AuthRouterOptions = { - provider: mockProvider, - issuerUrl: new URL('https://auth.example.com') - }; - app.use(mcpAuthRouter(options)); - vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('routes to authorization endpoint', async () => { - const response = await supertest(app).get('/authorize').query({ - client_id: 'valid-client', - response_type: 'code', - code_challenge: 'challenge123', - code_challenge_method: 'S256' - }); - - expect(response.status).toBe(302); - const location = new URL(response.header.location!); - expect(location.searchParams.has('code')).toBe(true); - }); - - it('routes to token endpoint', async () => { - // Setup verifyChallenge mock for token handler - vi.mock('pkce-challenge', () => ({ - verifyChallenge: vi.fn().mockResolvedValue(true) - })); - - const response = await supertest(app).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'valid_verifier' - }); - - // The request will fail in testing due to mocking limitations, - // but we can verify the route was matched - expect(response.status).not.toBe(404); - }); - - it('applies rate limiting to token endpoint (express-rate-limit)', async () => { - // Fresh app with a very low rate limit so we can trigger it deterministically - const limitedApp = express(); - const options = { - provider: mockProvider, - issuerUrl: new URL('https://auth.example.com'), - rateLimit: { windowMs: 60_000, max: 1 } - } as const; - limitedApp.use(mcpAuthRouter(options)); - - const first = await supertest(limitedApp).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'valid_verifier' - }); - expect(first.status).not.toBe(404); - - const second = await supertest(limitedApp).post('/token').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'valid_verifier' - }); - expect(second.status).toBe(429); - expect(second.body).toEqual(expect.objectContaining({ error: 'too_many_requests' })); - }); - - it('routes to registration endpoint', async () => { - const response = await supertest(app) - .post('/register') - .send({ - redirect_uris: ['https://example.com/callback'] - }); - - // The request will fail in testing due to mocking limitations, - // but we can verify the route was matched - expect(response.status).not.toBe(404); - }); - - it('routes to revocation endpoint', async () => { - const response = await supertest(app).post('/revoke').type('form').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - token: 'token_to_revoke' - }); - - // The request will fail in testing due to mocking limitations, - // but we can verify the route was matched - expect(response.status).not.toBe(404); - }); - - it('excludes endpoints for unsupported features', async () => { - // Setup minimal router - const minimalApp = express(); - const options: AuthRouterOptions = { - provider: mockProviderMinimal, - issuerUrl: new URL('https://auth.example.com') - }; - minimalApp.use(mcpAuthRouter(options)); - - // Registration should not be available - const regResponse = await supertest(minimalApp) - .post('/register') - .send({ - redirect_uris: ['https://example.com/callback'] - }); - expect(regResponse.status).toBe(404); - - // Revocation should not be available - const revokeResponse = await supertest(minimalApp).post('/revoke').send({ - client_id: 'valid-client', - client_secret: 'valid-secret', - token: 'token_to_revoke' - }); - expect(revokeResponse.status).toBe(404); - }); - }); -}); - -describe('MCP Auth Metadata Router', () => { - const mockOAuthMetadata: OAuthMetadata = { - issuer: 'https://auth.example.com/', - authorization_endpoint: 'https://auth.example.com/authorize', - token_endpoint: 'https://auth.example.com/token', - response_types_supported: ['code'], - grant_types_supported: ['authorization_code', 'refresh_token'], - code_challenge_methods_supported: ['S256'], - token_endpoint_auth_methods_supported: ['client_secret_post'] - }; - - describe('Router creation', () => { - it('successfully creates router with valid options', () => { - const options: AuthMetadataOptions = { - oauthMetadata: mockOAuthMetadata, - resourceServerUrl: new URL('https://api.example.com') - }; - - expect(() => mcpAuthMetadataRouter(options)).not.toThrow(); - }); - }); - - describe('Metadata endpoints', () => { - let app: express.Express; - - beforeEach(() => { - app = express(); - const options: AuthMetadataOptions = { - oauthMetadata: mockOAuthMetadata, - resourceServerUrl: new URL('https://api.example.com'), - serviceDocumentationUrl: new URL('https://docs.example.com'), - scopesSupported: ['read', 'write'], - resourceName: 'Test API' - }; - app.use(mcpAuthMetadataRouter(options)); - }); - - it('returns OAuth authorization server metadata', async () => { - const response = await supertest(app).get('/.well-known/oauth-authorization-server'); - - expect(response.status).toBe(200); - - // Verify metadata points to authorization server - expect(response.body.issuer).toBe('https://auth.example.com/'); - expect(response.body.authorization_endpoint).toBe('https://auth.example.com/authorize'); - expect(response.body.token_endpoint).toBe('https://auth.example.com/token'); - expect(response.body.response_types_supported).toEqual(['code']); - expect(response.body.grant_types_supported).toEqual(['authorization_code', 'refresh_token']); - expect(response.body.code_challenge_methods_supported).toEqual(['S256']); - expect(response.body.token_endpoint_auth_methods_supported).toEqual(['client_secret_post']); - }); - - it('returns OAuth protected resource metadata', async () => { - const response = await supertest(app).get('/.well-known/oauth-protected-resource'); - - expect(response.status).toBe(200); - - // Verify protected resource metadata - expect(response.body.resource).toBe('https://api.example.com/'); - expect(response.body.authorization_servers).toEqual(['https://auth.example.com/']); - expect(response.body.scopes_supported).toEqual(['read', 'write']); - expect(response.body.resource_name).toBe('Test API'); - expect(response.body.resource_documentation).toBe('https://docs.example.com/'); - }); - - it('works with minimal configuration', async () => { - const minimalApp = express(); - const options: AuthMetadataOptions = { - oauthMetadata: mockOAuthMetadata, - resourceServerUrl: new URL('https://api.example.com') - }; - minimalApp.use(mcpAuthMetadataRouter(options)); - - const authResponse = await supertest(minimalApp).get('/.well-known/oauth-authorization-server'); - - expect(authResponse.status).toBe(200); - expect(authResponse.body.issuer).toBe('https://auth.example.com/'); - expect(authResponse.body.service_documentation).toBeUndefined(); - expect(authResponse.body.scopes_supported).toBeUndefined(); - - const resourceResponse = await supertest(minimalApp).get('/.well-known/oauth-protected-resource'); - - expect(resourceResponse.status).toBe(200); - expect(resourceResponse.body.resource).toBe('https://api.example.com/'); - expect(resourceResponse.body.authorization_servers).toEqual(['https://auth.example.com/']); - expect(resourceResponse.body.scopes_supported).toBeUndefined(); - expect(resourceResponse.body.resource_name).toBeUndefined(); - expect(resourceResponse.body.resource_documentation).toBeUndefined(); - }); - }); -}); diff --git a/packages/server-hono/src/auth/bearerAuth.ts b/packages/server-hono/src/auth/bearerAuth.ts deleted file mode 100644 index 1258eee11..000000000 --- a/packages/server-hono/src/auth/bearerAuth.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { BearerAuthMiddlewareOptions } from '@modelcontextprotocol/server'; -import { requireBearerAuth as requireBearerAuthWeb } from '@modelcontextprotocol/server'; -import type { MiddlewareHandler } from 'hono'; -/** - * Hono middleware wrapper for the Web-standard `requireBearerAuth` helper. - * - * On success, sets `c.set('auth', authInfo)` and calls `next()`. - * On failure, returns the JSON error response. - */ -export function requireBearerAuth(options: BearerAuthMiddlewareOptions): MiddlewareHandler { - return async (c, next) => { - const result = await requireBearerAuthWeb(c.req.raw, options); - if ('authInfo' in result) { - c.set('auth', result.authInfo); - return await next(); - } - return result.response; - }; -} diff --git a/packages/server-hono/src/auth/router.ts b/packages/server-hono/src/auth/router.ts deleted file mode 100644 index f17765318..000000000 --- a/packages/server-hono/src/auth/router.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { AuthMetadataOptions, AuthRoute, AuthRouterOptions } from '@modelcontextprotocol/server'; -import { - getParsedBody, - mcpAuthMetadataRouter as createWebAuthMetadataRouter, - mcpAuthRouter as createWebAuthRouter -} from '@modelcontextprotocol/server'; -import type { Handler } from 'hono'; -import { Hono } from 'hono'; - -/** - * Hono router adapter for the Web-standard `mcpAuthRouter` from `@modelcontextprotocol/server`. - * - * IMPORTANT: This router MUST be mounted at the application root. - * - * @example - * ```ts - * app.route('/', mcpAuthRouter(...)) - * ``` - */ -export function mcpAuthRouter(options: AuthRouterOptions): Hono { - const web = createWebAuthRouter(options); - const router = new Hono(); - registerRoutes(router, web.routes); - return router; -} - -/** - * Hono router adapter for the Web-standard `mcpAuthMetadataRouter` from `@modelcontextprotocol/server`. - * - * IMPORTANT: This router MUST be mounted at the application root. - */ -export function mcpAuthMetadataRouter(options: AuthMetadataOptions): Hono { - const web = createWebAuthMetadataRouter(options); - const router = new Hono(); - registerRoutes(router, web.routes); - return router; -} - -function registerRoutes(app: Hono, routes: AuthRoute[]): void { - for (const route of routes) { - // Use `all()` so unsupported methods still reach the handler and can return 405, - // matching the Express adapter behavior. - const handler: Handler = async c => { - let parsedBody = c.get('parsedBody'); - if (parsedBody === undefined && c.req.method === 'POST') { - // Parse from a clone so we don't consume the original request stream. - parsedBody = await getParsedBody(c.req.raw.clone()); - } - return route.handler(c.req.raw, { parsedBody }); - }; - app.all(route.path, handler); - } -} - -export function registerMcpAuthRoutes(app: Hono, options: AuthRouterOptions): void { - app.route('/', mcpAuthRouter(options)); -} - -export function registerMcpAuthMetadataRoutes(app: Hono, options: AuthMetadataOptions): void { - app.route('/', mcpAuthMetadataRouter(options)); -} diff --git a/packages/server-hono/src/index.ts b/packages/server-hono/src/index.ts index bc6de4318..a8c65a2e9 100644 --- a/packages/server-hono/src/index.ts +++ b/packages/server-hono/src/index.ts @@ -1,5 +1,2 @@ -export * from './auth/bearerAuth.js'; -export * from './auth/router.js'; export * from './hono.js'; export * from './middleware/hostHeaderValidation.js'; -export * from './streamableHttp.js'; diff --git a/packages/server-hono/src/streamableHttp.ts b/packages/server-hono/src/streamableHttp.ts deleted file mode 100644 index 2da1bafcd..000000000 --- a/packages/server-hono/src/streamableHttp.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { getParsedBody } from '@modelcontextprotocol/server'; -import type { Context, Handler } from 'hono'; - -/** - * Convenience Hono handler for the WebStandard Streamable HTTP transport. - * - * Usage: - * ```ts - * app.all('/mcp', mcpStreamableHttpHandler(transport)) - * ``` - */ -export function mcpStreamableHttpHandler(transport: WebStandardStreamableHTTPServerTransport): Handler { - return async (c: Context) => { - let parsedBody = c.get('parsedBody'); - if (parsedBody === undefined && c.req.method === 'POST') { - // Parse from a clone so we don't consume the original request stream. - parsedBody = await getParsedBody(c.req.raw.clone()); - } - const authInfo = c.get('auth'); - return transport.handleRequest(c.req.raw, { authInfo, parsedBody }); - }; -} diff --git a/packages/server-hono/test/server-hono.test.ts b/packages/server-hono/test/server-hono.test.ts index 130e11c71..230a566ba 100644 --- a/packages/server-hono/test/server-hono.test.ts +++ b/packages/server-hono/test/server-hono.test.ts @@ -1,52 +1,11 @@ -import type { AuthorizationParams, OAuthClientInformationFull, OAuthServerProvider, OAuthTokens } from '@modelcontextprotocol/server'; import type { Context } from 'hono'; import { Hono } from 'hono'; import { vi } from 'vitest'; -import { mcpAuthRouter } from '../src/auth/router.js'; import { createMcpHonoApp } from '../src/hono.js'; import { hostHeaderValidation } from '../src/middleware/hostHeaderValidation.js'; -import { mcpStreamableHttpHandler } from '../src/streamableHttp.js'; describe('@modelcontextprotocol/server-hono', () => { - test('mcpStreamableHttpHandler delegates to transport.handleRequest (and passes authInfo + parsedBody when set)', async () => { - const calls: { url?: string; method?: string; options?: unknown }[] = []; - - const transport = { - async handleRequest(req: Request, options?: unknown): Promise { - calls.push({ url: req.url, method: req.method, options }); - return new Response('ok', { status: 200, headers: { 'content-type': 'text/plain' } }); - } - }; - - const app = new Hono(); - app.use('/mcp', async (c: Context, next) => { - // Upstream middleware can pre-parse and stash body + auth. - c.set('parsedBody', { hello: 'world' }); - c.set('auth', { - token: 't', - clientId: 'c', - scopes: [], - expiresAt: Math.floor(Date.now() / 1000) + 60 - }); - return await next(); - }); - app.all('/mcp', mcpStreamableHttpHandler(transport as unknown as Parameters[0])); - - const res = await app.request('http://localhost/mcp', { method: 'POST' }); - expect(res.status).toBe(200); - expect(await res.text()).toBe('ok'); - expect(calls).toHaveLength(1); - expect(calls[0]!.method).toBe('POST'); - expect(calls[0]!.url).toBe('http://localhost/mcp'); - expect(calls[0]!.options).toEqual( - expect.objectContaining({ - parsedBody: { hello: 'world' }, - authInfo: expect.objectContaining({ clientId: 'c' }) - }) - ); - }); - test('hostHeaderValidation blocks invalid Host and allows valid Host', async () => { const app = new Hono(); app.use('*', hostHeaderValidation(['localhost'])); @@ -69,165 +28,6 @@ describe('@modelcontextprotocol/server-hono', () => { expect(await good.text()).toBe('ok'); }); - test('registerMcpAuthRoutes mounts metadata + authorize routes', async () => { - const validClient: OAuthClientInformationFull = { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - - const provider: OAuthServerProvider = { - clientsStore: { - async getClient(clientId: string) { - return clientId === 'valid-client' ? validClient : undefined; - } - }, - async authorize(_client: OAuthClientInformationFull, params: AuthorizationParams): Promise { - const u = new URL(params.redirectUri); - u.searchParams.set('code', 'mock_auth_code'); - if (params.state) u.searchParams.set('state', params.state); - return Response.redirect(u.toString(), 302); - }, - async challengeForAuthorizationCode(): Promise { - return 'mock_challenge'; - }, - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - async verifyAccessToken() { - throw new Error('not used'); - } - }; - - const app = new Hono(); - app.route('/', mcpAuthRouter({ provider, issuerUrl: new URL('https://auth.example.com') })); - - const metadata = await app.request('http://localhost/.well-known/oauth-authorization-server', { method: 'GET' }); - expect(metadata.status).toBe(200); - const metaJson = (await metadata.json()) as { issuer?: string; authorization_endpoint?: string }; - expect(metaJson.issuer).toBe('https://auth.example.com/'); - expect(metaJson.authorization_endpoint).toBe('https://auth.example.com/authorize'); - - const authorize = await app.request( - 'http://localhost/authorize?client_id=valid-client&response_type=code&code_challenge=x&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&state=s', - { method: 'GET' } - ); - expect(authorize.status).toBe(302); - const location = authorize.headers.get('location')!; - expect(location).toContain('https://example.com/callback'); - expect(location).toContain('code=mock_auth_code'); - expect(location).toContain('state=s'); - }); - - test('registerMcpAuthRoutes returns 405 (not 404) for unsupported methods', async () => { - const provider: OAuthServerProvider = { - clientsStore: { - async getClient() { - return undefined; - } - }, - async authorize() { - throw new Error('not used'); - }, - async challengeForAuthorizationCode() { - throw new Error('not used'); - }, - async exchangeAuthorizationCode() { - throw new Error('not used'); - }, - async exchangeRefreshToken() { - throw new Error('not used'); - }, - async verifyAccessToken() { - throw new Error('not used'); - } - }; - - const app = new Hono(); - app.route('/', mcpAuthRouter({ provider, issuerUrl: new URL('https://auth.example.com') })); - - const res = await app.request('http://localhost/authorize', { method: 'PUT' }); - expect(res.status).toBe(405); - }); - - test('registerMcpAuthRoutes passes parsedBody to web handlers (POST /authorize works with empty raw body)', async () => { - const validClient: OAuthClientInformationFull = { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - - const provider: OAuthServerProvider = { - clientsStore: { - async getClient(clientId: string) { - return clientId === 'valid-client' ? validClient : undefined; - } - }, - async authorize(_client: OAuthClientInformationFull, params: AuthorizationParams): Promise { - const u = new URL(params.redirectUri); - u.searchParams.set('code', 'mock_auth_code'); - if (params.state) u.searchParams.set('state', params.state); - return Response.redirect(u.toString(), 302); - }, - async challengeForAuthorizationCode(): Promise { - return 'mock_challenge'; - }, - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - async verifyAccessToken() { - throw new Error('not used'); - } - }; - - const app = new Hono(); - app.use('/authorize', async (c: Context, next) => { - c.set('parsedBody', { - client_id: 'valid-client', - response_type: 'code', - code_challenge: 'x', - code_challenge_method: 'S256', - redirect_uri: 'https://example.com/callback', - state: 's' - }); - return await next(); - }); - app.route('/', mcpAuthRouter({ provider, issuerUrl: new URL('https://auth.example.com') })); - - const authorize = await app.request('http://localhost/authorize', { method: 'POST' }); - expect(authorize.status).toBe(302); - const location = authorize.headers.get('location')!; - expect(location).toContain('https://example.com/callback'); - expect(location).toContain('code=mock_auth_code'); - expect(location).toContain('state=s'); - }); - test('createMcpHonoApp enables localhost DNS rebinding protection by default', async () => { const app = createMcpHonoApp(); app.get('/health', c => c.text('ok')); @@ -276,4 +76,34 @@ describe('@modelcontextprotocol/server-hono', () => { expect(res.status).toBe(200); expect(await res.json()).toEqual({ a: 1 }); }); + + test('createMcpHonoApp returns 400 on invalid JSON', async () => { + const app = createMcpHonoApp(); + app.post('/echo', (c: Context) => c.text('ok')); + + const res = await app.request('http://localhost/echo', { + method: 'POST', + headers: { Host: 'localhost:3000', 'content-type': 'application/json' }, + body: '{"a":' + }); + expect(res.status).toBe(400); + expect(await res.text()).toBe('Invalid JSON'); + }); + + test('createMcpHonoApp does not override parsedBody if upstream middleware set it', async () => { + const app = createMcpHonoApp(); + app.use('/echo', async (c: Context, next) => { + c.set('parsedBody', { preset: true }); + return await next(); + }); + app.post('/echo', (c: Context) => c.json(c.get('parsedBody'))); + + const res = await app.request('http://localhost/echo', { + method: 'POST', + headers: { Host: 'localhost:3000', 'content-type': 'application/json' }, + body: JSON.stringify({ a: 1 }) + }); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ preset: true }); + }); }); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 667ba49d3..52ec221e2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,15 +1,12 @@ export * from './server/completable.js'; +export * from './server/helper/body.js'; export * from './server/mcp.js'; export * from './server/middleware/hostHeaderValidation.js'; export * from './server/server.js'; -export * from './server/sse.js'; export * from './server/stdio.js'; export * from './server/streamableHttp.js'; export * from './server/webStandardStreamableHttp.js'; -// auth exports -export * from './server/auth/index.js'; - // experimental exports export * from './experimental/index.js'; diff --git a/packages/server/src/server/auth/clients.ts b/packages/server/src/server/auth/clients.ts deleted file mode 100644 index f6aca1be9..000000000 --- a/packages/server/src/server/auth/clients.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; - -/** - * Stores information about registered OAuth clients for this server. - */ -export interface OAuthRegisteredClientsStore { - /** - * Returns information about a registered client, based on its ID. - */ - getClient(clientId: string): OAuthClientInformationFull | undefined | Promise; - - /** - * Registers a new client with the server. The client ID and secret will be automatically generated by the library. A modified version of the client information can be returned to reflect specific values enforced by the server. - * - * NOTE: Implementations should NOT delete expired client secrets in-place. Auth middleware provided by this library will automatically check the `client_secret_expires_at` field and reject requests with expired secrets. Any custom logic for authenticating clients should check the `client_secret_expires_at` field as well. - * - * If unimplemented, dynamic client registration is unsupported. - */ - registerClient?( - client: Omit - ): OAuthClientInformationFull | Promise; -} diff --git a/packages/server/src/server/auth/handlers/authorize.ts b/packages/server/src/server/auth/handlers/authorize.ts deleted file mode 100644 index ecffee114..000000000 --- a/packages/server/src/server/auth/handlers/authorize.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { InvalidClientError, InvalidRequestError, OAuthError, ServerError } from '@modelcontextprotocol/core'; -import * as z from 'zod/v4'; - -import type { OAuthServerProvider } from '../provider.js'; -import type { WebHandler } from '../web.js'; -import { getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; - -export type AuthorizationHandlerOptions = { - provider: OAuthServerProvider; -}; - -// Parameters that must be validated in order to issue redirects. -const ClientAuthorizationParamsSchema = z.object({ - client_id: z.string(), - redirect_uri: z - .string() - .optional() - .refine(value => value === undefined || URL.canParse(value), { message: 'redirect_uri must be a valid URL' }) -}); - -// Parameters that must be validated for a successful authorization request. Failure can be reported to the redirect URI. -const RequestAuthorizationParamsSchema = z.object({ - response_type: z.literal('code'), - code_challenge: z.string(), - code_challenge_method: z.literal('S256'), - scope: z.string().optional(), - state: z.string().optional(), - resource: z.string().url().optional() -}); - -export function authorizationHandler({ provider }: AuthorizationHandlerOptions): WebHandler { - return async (req, ctx) => { - const noStore = noStoreHeaders(); - - if (req.method !== 'GET' && req.method !== 'POST') { - const resp = methodNotAllowedResponse(req, ['GET', 'POST']); - const body = await resp.text(); - return new Response(body, { - status: resp.status, - headers: { ...Object.fromEntries(resp.headers.entries()), ...noStore } - }); - } - - // In the authorization flow, errors are split into two categories: - // 1. Pre-redirect errors (direct response with 400) - // 2. Post-redirect errors (redirect with error parameters) - - // Phase 1: Validate client_id and redirect_uri. Any errors here must be direct responses. - let client_id, redirect_uri, client; - try { - const source = - req.method === 'POST' ? await getParsedBody(req, ctx) : Object.fromEntries(new URL(req.url).searchParams.entries()); - const result = ClientAuthorizationParamsSchema.safeParse(source); - if (!result.success) { - throw new InvalidRequestError(result.error.message); - } - - client_id = result.data.client_id; - redirect_uri = result.data.redirect_uri; - - client = await provider.clientsStore.getClient(client_id); - if (!client) { - throw new InvalidClientError('Invalid client_id'); - } - - if (redirect_uri !== undefined) { - if (!client.redirect_uris.includes(redirect_uri)) { - throw new InvalidRequestError('Unregistered redirect_uri'); - } - } else if (client.redirect_uris.length === 1) { - redirect_uri = client.redirect_uris[0]; - } else { - throw new InvalidRequestError('redirect_uri must be specified when client has multiple registered URIs'); - } - } catch (error) { - // Pre-redirect errors - return direct response - // - // These don't need to be JSON encoded, as they'll be displayed in a user - // agent, but OTOH they all represent exceptional situations (arguably, - // "programmer error"), so presenting a nice HTML page doesn't help the - // user anyway. - if (error instanceof OAuthError) { - const status = error instanceof ServerError ? 500 : 400; - return jsonResponse(error.toResponseObject(), { status, headers: noStore }); - } else { - const serverError = new ServerError('Internal Server Error'); - return jsonResponse(serverError.toResponseObject(), { status: 500, headers: noStore }); - } - } - - // Phase 2: Validate other parameters. Any errors here should go into redirect responses. - let state; - try { - // Parse and validate authorization parameters - const source = - req.method === 'POST' ? await getParsedBody(req, ctx) : Object.fromEntries(new URL(req.url).searchParams.entries()); - const parseResult = RequestAuthorizationParamsSchema.safeParse(source); - if (!parseResult.success) { - throw new InvalidRequestError(parseResult.error.message); - } - - const { scope, code_challenge, resource } = parseResult.data; - state = parseResult.data.state; - - // Validate scopes - let requestedScopes: string[] = []; - if (scope !== undefined) { - requestedScopes = scope.split(' '); - } - - // All validation passed, proceed with authorization - const providerResponse = await provider.authorize(client, { - state, - scopes: requestedScopes, - redirectUri: redirect_uri!, // TODO: Someone to look at. Strict tsconfig showed this could be undefined, while the return type is string. - codeChallenge: code_challenge, - resource: resource ? new URL(resource) : undefined - }); - const headers = new Headers(providerResponse.headers); - headers.set('Cache-Control', 'no-store'); - return new Response(providerResponse.body, { status: providerResponse.status, headers }); - } catch (error) { - // Post-redirect errors - redirect with error parameters - if (error instanceof OAuthError) { - const location = createErrorRedirect(redirect_uri!, error, state); - return new Response(null, { status: 302, headers: { Location: location, ...noStore } }); - } else { - const serverError = new ServerError('Internal Server Error'); - const location = createErrorRedirect(redirect_uri!, serverError, state); - return new Response(null, { status: 302, headers: { Location: location, ...noStore } }); - } - } - }; -} - -/** - * Helper function to create redirect URL with error parameters - */ -function createErrorRedirect(redirectUri: string, error: OAuthError, state?: string): string { - const errorUrl = new URL(redirectUri); - errorUrl.searchParams.set('error', error.errorCode); - errorUrl.searchParams.set('error_description', error.message); - if (error.errorUri) { - errorUrl.searchParams.set('error_uri', error.errorUri); - } - if (state) { - errorUrl.searchParams.set('state', state); - } - return errorUrl.href; -} diff --git a/packages/server/src/server/auth/handlers/metadata.ts b/packages/server/src/server/auth/handlers/metadata.ts deleted file mode 100644 index fea42a8cb..000000000 --- a/packages/server/src/server/auth/handlers/metadata.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/core'; - -import type { WebHandler } from '../web.js'; -import { corsHeaders, corsPreflightResponse, jsonResponse, methodNotAllowedResponse } from '../web.js'; - -export function metadataHandler(metadata: OAuthMetadata | OAuthProtectedResourceMetadata): WebHandler { - const cors = { - allowOrigin: '*', - allowMethods: ['GET', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization'], - maxAgeSeconds: 60 * 60 * 24 - } as const; - - return async req => { - if (req.method === 'OPTIONS') { - return corsPreflightResponse(cors); - } - if (req.method !== 'GET') { - const resp = methodNotAllowedResponse(req, ['GET', 'OPTIONS']); - // Add CORS headers for consistency with successful responses. - const body = await resp.text(); - return new Response(body, { - status: resp.status, - headers: { ...Object.fromEntries(resp.headers.entries()), ...corsHeaders(cors) } - }); - } - - return jsonResponse(metadata, { - status: 200, - headers: corsHeaders(cors) - }); - }; -} diff --git a/packages/server/src/server/auth/handlers/register.ts b/packages/server/src/server/auth/handlers/register.ts deleted file mode 100644 index 4433a1b5b..000000000 --- a/packages/server/src/server/auth/handlers/register.ts +++ /dev/null @@ -1,105 +0,0 @@ -import crypto from 'node:crypto'; - -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import { InvalidClientMetadataError, OAuthClientMetadataSchema, OAuthError, ServerError } from '@modelcontextprotocol/core'; - -import type { OAuthRegisteredClientsStore } from '../clients.js'; -import type { WebHandler } from '../web.js'; -import { corsHeaders, corsPreflightResponse, getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; - -export type ClientRegistrationHandlerOptions = { - /** - * A store used to save information about dynamically registered OAuth clients. - */ - clientsStore: OAuthRegisteredClientsStore; - - /** - * The number of seconds after which to expire issued client secrets, or 0 to prevent expiration of client secrets (not recommended). - * - * If not set, defaults to 30 days. - */ - clientSecretExpirySeconds?: number; - - /** - * Whether to generate a client ID before calling the client registration endpoint. - * - * If not set, defaults to true. - */ - clientIdGeneration?: boolean; -}; - -const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days - -export function clientRegistrationHandler({ - clientsStore, - clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS, - clientIdGeneration = true -}: ClientRegistrationHandlerOptions): WebHandler { - if (!clientsStore.registerClient) { - throw new Error('Client registration store does not support registering clients'); - } - - const cors = { - allowOrigin: '*', - allowMethods: ['POST', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization'], - maxAgeSeconds: 60 * 60 * 24 - } as const; - - return async (req, ctx) => { - const baseHeaders = { ...corsHeaders(cors), ...noStoreHeaders() }; - - if (req.method === 'OPTIONS') { - return corsPreflightResponse(cors); - } - if (req.method !== 'POST') { - const resp = methodNotAllowedResponse(req, ['POST', 'OPTIONS']); - const body = await resp.text(); - return new Response(body, { - status: resp.status, - headers: { ...Object.fromEntries(resp.headers.entries()), ...baseHeaders } - }); - } - - try { - const rawBody = await getParsedBody(req, ctx); - const parseResult = OAuthClientMetadataSchema.safeParse(rawBody); - if (!parseResult.success) { - throw new InvalidClientMetadataError(parseResult.error.message); - } - - const clientMetadata = parseResult.data; - const isPublicClient = clientMetadata.token_endpoint_auth_method === 'none'; - - // Generate client credentials - const clientSecret = isPublicClient ? undefined : crypto.randomBytes(32).toString('hex'); - const clientIdIssuedAt = Math.floor(Date.now() / 1000); - - // Calculate client secret expiry time - const clientsDoExpire = clientSecretExpirySeconds > 0; - const secretExpiryTime = clientsDoExpire ? clientIdIssuedAt + clientSecretExpirySeconds : 0; - const clientSecretExpiresAt = isPublicClient ? undefined : secretExpiryTime; - - let clientInfo: Omit & { client_id?: string } = { - ...clientMetadata, - client_secret: clientSecret, - client_secret_expires_at: clientSecretExpiresAt - }; - - if (clientIdGeneration) { - clientInfo.client_id = crypto.randomUUID(); - clientInfo.client_id_issued_at = clientIdIssuedAt; - } - - clientInfo = await clientsStore.registerClient!(clientInfo); - return jsonResponse(clientInfo, { status: 201, headers: baseHeaders }); - } catch (error) { - if (error instanceof OAuthError) { - const status = error instanceof ServerError ? 500 : 400; - return jsonResponse(error.toResponseObject(), { status, headers: baseHeaders }); - } - const serverError = new ServerError('Internal Server Error'); - return jsonResponse(serverError.toResponseObject(), { status: 500, headers: baseHeaders }); - } - }; -} diff --git a/packages/server/src/server/auth/handlers/revoke.ts b/packages/server/src/server/auth/handlers/revoke.ts deleted file mode 100644 index e4814345d..000000000 --- a/packages/server/src/server/auth/handlers/revoke.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { InvalidRequestError, OAuthError, OAuthTokenRevocationRequestSchema, ServerError } from '@modelcontextprotocol/core'; - -import { authenticateClient } from '../middleware/clientAuth.js'; -import type { OAuthServerProvider } from '../provider.js'; -import type { WebHandler } from '../web.js'; -import { corsHeaders, corsPreflightResponse, getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; - -export type RevocationHandlerOptions = { - provider: OAuthServerProvider; -}; - -export function revocationHandler({ provider }: RevocationHandlerOptions): WebHandler { - if (!provider.revokeToken) { - throw new Error('Auth provider does not support revoking tokens'); - } - - const cors = { - allowOrigin: '*', - allowMethods: ['POST', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization'], - maxAgeSeconds: 60 * 60 * 24 - } as const; - - return async (req, ctx) => { - const baseHeaders = { ...corsHeaders(cors), ...noStoreHeaders() }; - - if (req.method === 'OPTIONS') { - return corsPreflightResponse(cors); - } - if (req.method !== 'POST') { - const resp = methodNotAllowedResponse(req, ['POST', 'OPTIONS']); - const body = await resp.text(); - return new Response(body, { - status: resp.status, - headers: { ...Object.fromEntries(resp.headers.entries()), ...baseHeaders } - }); - } - - try { - const rawBody = await getParsedBody(req, ctx); - const parseResult = OAuthTokenRevocationRequestSchema.safeParse(rawBody); - if (!parseResult.success) { - throw new InvalidRequestError(parseResult.error.message); - } - - const client = await authenticateClient(rawBody, { clientsStore: provider.clientsStore }); - - await provider.revokeToken!(client, parseResult.data); - return jsonResponse({}, { status: 200, headers: baseHeaders }); - } catch (error) { - if (error instanceof OAuthError) { - const status = error instanceof ServerError ? 500 : 400; - return jsonResponse(error.toResponseObject(), { status, headers: baseHeaders }); - } - const serverError = new ServerError('Internal Server Error'); - return jsonResponse(serverError.toResponseObject(), { status: 500, headers: baseHeaders }); - } - }; -} diff --git a/packages/server/src/server/auth/handlers/token.ts b/packages/server/src/server/auth/handlers/token.ts deleted file mode 100644 index 6dcdfd8b1..000000000 --- a/packages/server/src/server/auth/handlers/token.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { InvalidGrantError, InvalidRequestError, OAuthError, ServerError, UnsupportedGrantTypeError } from '@modelcontextprotocol/core'; -import { verifyChallenge } from 'pkce-challenge'; -import * as z from 'zod/v4'; - -import { authenticateClient } from '../middleware/clientAuth.js'; -import type { OAuthServerProvider } from '../provider.js'; -import type { WebHandler } from '../web.js'; -import { corsHeaders, corsPreflightResponse, getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js'; - -export type TokenHandlerOptions = { - provider: OAuthServerProvider; -}; - -const TokenRequestSchema = z.object({ - grant_type: z.string() -}); - -const AuthorizationCodeGrantSchema = z.object({ - code: z.string(), - code_verifier: z.string(), - redirect_uri: z.string().optional(), - resource: z.string().url().optional() -}); - -const RefreshTokenGrantSchema = z.object({ - refresh_token: z.string(), - scope: z.string().optional(), - resource: z.string().url().optional() -}); - -export function tokenHandler({ provider }: TokenHandlerOptions): WebHandler { - const cors = { - allowOrigin: '*', - allowMethods: ['POST', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization'], - maxAgeSeconds: 60 * 60 * 24 - } as const; - - return async (req, ctx) => { - const baseHeaders = { ...corsHeaders(cors), ...noStoreHeaders() }; - - if (req.method === 'OPTIONS') { - return corsPreflightResponse(cors); - } - if (req.method !== 'POST') { - const resp = methodNotAllowedResponse(req, ['POST', 'OPTIONS']); - const body = await resp.text(); - return new Response(body, { - status: resp.status, - headers: { ...Object.fromEntries(resp.headers.entries()), ...baseHeaders } - }); - } - - try { - const rawBody = await getParsedBody(req, ctx); - const parseResult = TokenRequestSchema.safeParse(rawBody); - if (!parseResult.success) { - throw new InvalidRequestError(parseResult.error.message); - } - - const { grant_type } = parseResult.data; - - const client = await authenticateClient(rawBody, { clientsStore: provider.clientsStore }); - - switch (grant_type) { - case 'authorization_code': { - const parseResult = AuthorizationCodeGrantSchema.safeParse(rawBody); - if (!parseResult.success) { - throw new InvalidRequestError(parseResult.error.message); - } - - const { code, code_verifier, redirect_uri, resource } = parseResult.data; - - const skipLocalPkceValidation = provider.skipLocalPkceValidation; - - // Perform local PKCE validation unless explicitly skipped - // (e.g. to validate code_verifier in upstream server) - if (!skipLocalPkceValidation) { - const codeChallenge = await provider.challengeForAuthorizationCode(client, code); - if (!(await verifyChallenge(code_verifier, codeChallenge))) { - throw new InvalidGrantError('code_verifier does not match the challenge'); - } - } - - // Passes the code_verifier to the provider if PKCE validation didn't occur locally - const tokens = await provider.exchangeAuthorizationCode( - client, - code, - skipLocalPkceValidation ? code_verifier : undefined, - redirect_uri, - resource ? new URL(resource) : undefined - ); - return jsonResponse(tokens, { status: 200, headers: baseHeaders }); - } - - case 'refresh_token': { - const parseResult = RefreshTokenGrantSchema.safeParse(rawBody); - if (!parseResult.success) { - throw new InvalidRequestError(parseResult.error.message); - } - - const { refresh_token, scope, resource } = parseResult.data; - - const scopes = scope?.split(' '); - const tokens = await provider.exchangeRefreshToken( - client, - refresh_token, - scopes, - resource ? new URL(resource) : undefined - ); - return jsonResponse(tokens, { status: 200, headers: baseHeaders }); - } - // Additional auth methods will not be added on the server side of the SDK. - case 'client_credentials': - default: - throw new UnsupportedGrantTypeError('The grant type is not supported by this authorization server.'); - } - } catch (error) { - if (error instanceof OAuthError) { - const status = error instanceof ServerError ? 500 : 400; - return jsonResponse(error.toResponseObject(), { status, headers: baseHeaders }); - } - const serverError = new ServerError('Internal Server Error'); - return jsonResponse(serverError.toResponseObject(), { status: 500, headers: baseHeaders }); - } - }; -} diff --git a/packages/server/src/server/auth/index.ts b/packages/server/src/server/auth/index.ts deleted file mode 100644 index 2b176805b..000000000 --- a/packages/server/src/server/auth/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './clients.js'; -export * from './handlers/authorize.js'; -export * from './handlers/metadata.js'; -export * from './handlers/register.js'; -export * from './handlers/revoke.js'; -export * from './handlers/token.js'; -export * from './middleware/allowedMethods.js'; -export * from './middleware/bearerAuth.js'; -export * from './middleware/clientAuth.js'; -export * from './provider.js'; -export * from './providers/proxyProvider.js'; -export * from './router.js'; -export * from './web.js'; diff --git a/packages/server/src/server/auth/middleware/allowedMethods.ts b/packages/server/src/server/auth/middleware/allowedMethods.ts deleted file mode 100644 index 5c5245690..000000000 --- a/packages/server/src/server/auth/middleware/allowedMethods.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MethodNotAllowedError } from '@modelcontextprotocol/core'; - -import { jsonResponse } from '../web.js'; - -/** - * Helper to handle unsupported HTTP methods with a 405 Method Not Allowed response. - * - * @param allowedMethods Array of allowed HTTP methods for this endpoint (e.g., ['GET', 'POST']) - * @returns Response if method not in allowed list, otherwise undefined - */ -export function allowedMethods(allowedMethods: string[], req: Request): Response | undefined { - if (allowedMethods.includes(req.method)) { - return undefined; - } - const error = new MethodNotAllowedError(`The method ${req.method} is not allowed for this endpoint`); - return jsonResponse(error.toResponseObject(), { - status: 405, - headers: { Allow: allowedMethods.join(', ') } - }); -} diff --git a/packages/server/src/server/auth/middleware/bearerAuth.ts b/packages/server/src/server/auth/middleware/bearerAuth.ts deleted file mode 100644 index 853e400f6..000000000 --- a/packages/server/src/server/auth/middleware/bearerAuth.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { AuthInfo } from '@modelcontextprotocol/core'; -import { InsufficientScopeError, InvalidTokenError, OAuthError, ServerError } from '@modelcontextprotocol/core'; - -import type { OAuthTokenVerifier } from '../provider.js'; -import { jsonResponse } from '../web.js'; - -export type BearerAuthMiddlewareOptions = { - /** - * A provider used to verify tokens. - */ - verifier: OAuthTokenVerifier; - - /** - * Optional scopes that the token must have. - */ - requiredScopes?: string[]; - - /** - * Optional resource metadata URL to include in WWW-Authenticate header. - */ - resourceMetadataUrl?: string; -}; - -/** - * Validates a Bearer token in the Authorization header. - * - * Returns either `{ authInfo }` on success or `{ response }` on failure. - */ -export async function requireBearerAuth( - req: Request, - { verifier, requiredScopes = [], resourceMetadataUrl }: BearerAuthMiddlewareOptions -): Promise<{ authInfo: AuthInfo } | { response: Response }> { - try { - const authHeader = req.headers.get('authorization'); - if (!authHeader) { - throw new InvalidTokenError('Missing Authorization header'); - } - - const [type, token] = authHeader.split(' '); - if (type!.toLowerCase() !== 'bearer' || !token) { - throw new InvalidTokenError("Invalid Authorization header format, expected 'Bearer TOKEN'"); - } - - const authInfo = await verifier.verifyAccessToken(token); - - // Check if token has the required scopes (if any) - if (requiredScopes.length > 0) { - const hasAllScopes = requiredScopes.every(scope => authInfo.scopes.includes(scope)); - - if (!hasAllScopes) { - throw new InsufficientScopeError('Insufficient scope'); - } - } - - // Check if the token is set to expire or if it is expired - if (typeof authInfo.expiresAt !== 'number' || isNaN(authInfo.expiresAt)) { - throw new InvalidTokenError('Token has no expiration time'); - } else if (authInfo.expiresAt < Date.now() / 1000) { - throw new InvalidTokenError('Token has expired'); - } - - return { authInfo }; - } catch (error) { - // Build WWW-Authenticate header parts - const buildWwwAuthHeader = (errorCode: string, message: string): string => { - let header = `Bearer error="${errorCode}", error_description="${message}"`; - if (requiredScopes.length > 0) { - header += `, scope="${requiredScopes.join(' ')}"`; - } - if (resourceMetadataUrl) { - header += `, resource_metadata="${resourceMetadataUrl}"`; - } - return header; - }; - - if (error instanceof InvalidTokenError) { - return { - response: jsonResponse(error.toResponseObject(), { - status: 401, - headers: { 'WWW-Authenticate': buildWwwAuthHeader(error.errorCode, error.message) } - }) - }; - } - if (error instanceof InsufficientScopeError) { - return { - response: jsonResponse(error.toResponseObject(), { - status: 403, - headers: { 'WWW-Authenticate': buildWwwAuthHeader(error.errorCode, error.message) } - }) - }; - } - if (error instanceof ServerError) { - return { response: jsonResponse(error.toResponseObject(), { status: 500 }) }; - } - if (error instanceof OAuthError) { - return { response: jsonResponse(error.toResponseObject(), { status: 400 }) }; - } - const serverError = new ServerError('Internal Server Error'); - return { response: jsonResponse(serverError.toResponseObject(), { status: 500 }) }; - } -} diff --git a/packages/server/src/server/auth/middleware/clientAuth.ts b/packages/server/src/server/auth/middleware/clientAuth.ts deleted file mode 100644 index 9da271e35..000000000 --- a/packages/server/src/server/auth/middleware/clientAuth.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import { InvalidClientError, InvalidRequestError } from '@modelcontextprotocol/core'; -import * as z from 'zod/v4'; - -import type { OAuthRegisteredClientsStore } from '../clients.js'; - -export type ClientAuthenticationMiddlewareOptions = { - /** - * A store used to read information about registered OAuth clients. - */ - clientsStore: OAuthRegisteredClientsStore; -}; - -const ClientAuthenticatedRequestSchema = z.object({ - client_id: z.string(), - client_secret: z.string().optional() -}); - -/** - * Parses and validates client credentials from a request body, returning the authenticated client. - * - * Throws an OAuthError (or ServerError) on failure. - */ -export async function authenticateClient( - body: unknown, - { clientsStore }: ClientAuthenticationMiddlewareOptions -): Promise { - const result = ClientAuthenticatedRequestSchema.safeParse(body); - if (!result.success) { - throw new InvalidRequestError(String(result.error)); - } - const { client_id, client_secret } = result.data; - const client = await clientsStore.getClient(client_id); - if (!client) { - throw new InvalidClientError('Invalid client_id'); - } - if (client.client_secret) { - if (!client_secret) { - throw new InvalidClientError('Client secret is required'); - } - if (client.client_secret !== client_secret) { - throw new InvalidClientError('Invalid client_secret'); - } - if (client.client_secret_expires_at && client.client_secret_expires_at < Math.floor(Date.now() / 1000)) { - throw new InvalidClientError('Client secret has expired'); - } - } - - return client; -} diff --git a/packages/server/src/server/auth/provider.ts b/packages/server/src/server/auth/provider.ts deleted file mode 100644 index d7dc395d1..000000000 --- a/packages/server/src/server/auth/provider.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { AuthInfo, OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; - -import type { OAuthRegisteredClientsStore } from './clients.js'; - -export type AuthorizationParams = { - state?: string; - scopes?: string[]; - codeChallenge: string; - redirectUri: string; - resource?: URL; -}; - -/** - * Implements an end-to-end OAuth server. - */ -export interface OAuthServerProvider { - /** - * A store used to read information about registered OAuth clients. - */ - get clientsStore(): OAuthRegisteredClientsStore; - - /** - * Begins the authorization flow, which can either be implemented by this server itself or via redirection to a separate authorization server. - * - * This server must eventually issue a redirect with an authorization response or an error response to the given redirect URI. Per OAuth 2.1: - * - In the successful case, the redirect MUST include the `code` and `state` (if present) query parameters. - * - In the error case, the redirect MUST include the `error` query parameter, and MAY include an optional `error_description` query parameter. - */ - authorize(client: OAuthClientInformationFull, params: AuthorizationParams): Promise; - - /** - * Returns the `codeChallenge` that was used when the indicated authorization began. - */ - challengeForAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise; - - /** - * Exchanges an authorization code for an access token. - */ - exchangeAuthorizationCode( - client: OAuthClientInformationFull, - authorizationCode: string, - codeVerifier?: string, - redirectUri?: string, - resource?: URL - ): Promise; - - /** - * Exchanges a refresh token for an access token. - */ - exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, scopes?: string[], resource?: URL): Promise; - - /** - * Verifies an access token and returns information about it. - */ - verifyAccessToken(token: string): Promise; - - /** - * Revokes an access or refresh token. If unimplemented, token revocation is not supported (not recommended). - * - * If the given token is invalid or already revoked, this method should do nothing. - */ - revokeToken?(client: OAuthClientInformationFull, request: OAuthTokenRevocationRequest): Promise; - - /** - * Whether to skip local PKCE validation. - * - * If true, the server will not perform PKCE validation locally and will pass the code_verifier to the upstream server. - * - * NOTE: This should only be true if the upstream server is performing the actual PKCE validation. - */ - skipLocalPkceValidation?: boolean; -} - -/** - * Slim implementation useful for token verification - */ -export interface OAuthTokenVerifier { - /** - * Verifies an access token and returns information about it. - */ - verifyAccessToken(token: string): Promise; -} diff --git a/packages/server/src/server/auth/providers/proxyProvider.ts b/packages/server/src/server/auth/providers/proxyProvider.ts deleted file mode 100644 index 230e8766e..000000000 --- a/packages/server/src/server/auth/providers/proxyProvider.ts +++ /dev/null @@ -1,230 +0,0 @@ -import type { AuthInfo, FetchLike, OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; -import { OAuthClientInformationFullSchema, OAuthTokensSchema, ServerError } from '@modelcontextprotocol/core'; - -import type { OAuthRegisteredClientsStore } from '../clients.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../provider.js'; - -export type ProxyEndpoints = { - authorizationUrl: string; - tokenUrl: string; - revocationUrl?: string; - registrationUrl?: string; -}; - -export type ProxyOptions = { - /** - * Individual endpoint URLs for proxying specific OAuth operations - */ - endpoints: ProxyEndpoints; - - /** - * Function to verify access tokens and return auth info - */ - verifyAccessToken: (token: string) => Promise; - - /** - * Function to fetch client information from the upstream server - */ - getClient: (clientId: string) => Promise; - - /** - * Custom fetch implementation used for all network requests. - */ - fetch?: FetchLike; -}; - -/** - * Implements an OAuth server that proxies requests to another OAuth server. - */ -export class ProxyOAuthServerProvider implements OAuthServerProvider { - protected readonly _endpoints: ProxyEndpoints; - protected readonly _verifyAccessToken: (token: string) => Promise; - protected readonly _getClient: (clientId: string) => Promise; - protected readonly _fetch?: FetchLike; - - skipLocalPkceValidation = true; - - revokeToken?: (client: OAuthClientInformationFull, request: OAuthTokenRevocationRequest) => Promise; - - constructor(options: ProxyOptions) { - this._endpoints = options.endpoints; - this._verifyAccessToken = options.verifyAccessToken; - this._getClient = options.getClient; - this._fetch = options.fetch; - if (options.endpoints?.revocationUrl) { - this.revokeToken = async (client: OAuthClientInformationFull, request: OAuthTokenRevocationRequest) => { - const revocationUrl = this._endpoints.revocationUrl; - - if (!revocationUrl) { - throw new Error('No revocation endpoint configured'); - } - - const params = new URLSearchParams(); - params.set('token', request.token); - params.set('client_id', client.client_id); - if (client.client_secret) { - params.set('client_secret', client.client_secret); - } - if (request.token_type_hint) { - params.set('token_type_hint', request.token_type_hint); - } - - const response = await (this._fetch ?? fetch)(revocationUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: params.toString() - }); - await response.body?.cancel(); - - if (!response.ok) { - throw new ServerError(`Token revocation failed: ${response.status}`); - } - }; - } - } - - get clientsStore(): OAuthRegisteredClientsStore { - const registrationUrl = this._endpoints.registrationUrl; - return { - getClient: this._getClient, - ...(registrationUrl && { - registerClient: async (client: OAuthClientInformationFull) => { - const response = await (this._fetch ?? fetch)(registrationUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(client) - }); - - if (!response.ok) { - await response.body?.cancel(); - throw new ServerError(`Client registration failed: ${response.status}`); - } - - const data = await response.json(); - return OAuthClientInformationFullSchema.parse(data); - } - }) - }; - } - - async authorize(client: OAuthClientInformationFull, params: AuthorizationParams): Promise { - // Start with required OAuth parameters - const targetUrl = new URL(this._endpoints.authorizationUrl); - const searchParams = new URLSearchParams({ - client_id: client.client_id, - response_type: 'code', - redirect_uri: params.redirectUri, - code_challenge: params.codeChallenge, - code_challenge_method: 'S256' - }); - - // Add optional standard OAuth parameters - if (params.state) searchParams.set('state', params.state); - if (params.scopes?.length) searchParams.set('scope', params.scopes.join(' ')); - if (params.resource) searchParams.set('resource', params.resource.href); - - targetUrl.search = searchParams.toString(); - return Response.redirect(targetUrl.toString(), 302); - } - - async challengeForAuthorizationCode(_client: OAuthClientInformationFull, _authorizationCode: string): Promise { - // In a proxy setup, we don't store the code challenge ourselves - // Instead, we proxy the token request and let the upstream server validate it - return ''; - } - - async exchangeAuthorizationCode( - client: OAuthClientInformationFull, - authorizationCode: string, - codeVerifier?: string, - redirectUri?: string, - resource?: URL - ): Promise { - const params = new URLSearchParams({ - grant_type: 'authorization_code', - client_id: client.client_id, - code: authorizationCode - }); - - if (client.client_secret) { - params.append('client_secret', client.client_secret); - } - - if (codeVerifier) { - params.append('code_verifier', codeVerifier); - } - - if (redirectUri) { - params.append('redirect_uri', redirectUri); - } - - if (resource) { - params.append('resource', resource.href); - } - - const response = await (this._fetch ?? fetch)(this._endpoints.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: params.toString() - }); - - if (!response.ok) { - await response.body?.cancel(); - throw new ServerError(`Token exchange failed: ${response.status}`); - } - - const data = await response.json(); - return OAuthTokensSchema.parse(data); - } - - async exchangeRefreshToken( - client: OAuthClientInformationFull, - refreshToken: string, - scopes?: string[], - resource?: URL - ): Promise { - const params = new URLSearchParams({ - grant_type: 'refresh_token', - client_id: client.client_id, - refresh_token: refreshToken - }); - - if (client.client_secret) { - params.set('client_secret', client.client_secret); - } - - if (scopes?.length) { - params.set('scope', scopes.join(' ')); - } - - if (resource) { - params.set('resource', resource.href); - } - - const response = await (this._fetch ?? fetch)(this._endpoints.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: params.toString() - }); - - if (!response.ok) { - await response.body?.cancel(); - throw new ServerError(`Token refresh failed: ${response.status}`); - } - - const data = await response.json(); - return OAuthTokensSchema.parse(data); - } - - async verifyAccessToken(token: string): Promise { - return this._verifyAccessToken(token); - } -} diff --git a/packages/server/src/server/auth/router.ts b/packages/server/src/server/auth/router.ts deleted file mode 100644 index 61ed79806..000000000 --- a/packages/server/src/server/auth/router.ts +++ /dev/null @@ -1,280 +0,0 @@ -import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/core'; - -import type { AuthorizationHandlerOptions } from './handlers/authorize.js'; -import { authorizationHandler } from './handlers/authorize.js'; -import { metadataHandler } from './handlers/metadata.js'; -import type { ClientRegistrationHandlerOptions } from './handlers/register.js'; -import { clientRegistrationHandler } from './handlers/register.js'; -import type { RevocationHandlerOptions } from './handlers/revoke.js'; -import { revocationHandler } from './handlers/revoke.js'; -import type { TokenHandlerOptions } from './handlers/token.js'; -import { tokenHandler } from './handlers/token.js'; -import type { OAuthServerProvider } from './provider.js'; -import type { WebHandler } from './web.js'; - -// Check for dev mode flag that allows HTTP issuer URLs (for development/testing only) -const allowInsecureIssuerUrl = - process.env.MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL === 'true' || process.env.MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL === '1'; -if (allowInsecureIssuerUrl) { - // eslint-disable-next-line no-console - console.warn('MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL is enabled - HTTP issuer URLs are allowed. Do not use in production.'); -} - -export type AuthRouterOptions = { - /** - * A provider implementing the actual authorization logic for this router. - */ - provider: OAuthServerProvider; - - /** - * The authorization server's issuer identifier, which is a URL that uses the "https" scheme and has no query or fragment components. - */ - issuerUrl: URL; - - /** - * The base URL of the authorization server to use for the metadata endpoints. - * - * If not provided, the issuer URL will be used as the base URL. - */ - baseUrl?: URL; - - /** - * An optional URL of a page containing human-readable information that developers might want or need to know when using the authorization server. - */ - serviceDocumentationUrl?: URL; - - /** - * An optional list of scopes supported by this authorization server - */ - scopesSupported?: string[]; - - /** - * The resource name to be displayed in protected resource metadata - */ - resourceName?: string; - - /** - * The URL of the protected resource (RS) whose metadata we advertise. - * If not provided, falls back to `baseUrl` and then to `issuerUrl` (AS=RS). - */ - resourceServerUrl?: URL; - - // Individual options per route - authorizationOptions?: Omit; - clientRegistrationOptions?: Omit; - revocationOptions?: Omit; - tokenOptions?: Omit; -}; - -export type AuthRoute = { - path: string; - methods: string[]; - handler: WebHandler; -}; - -export type WebAuthRouter = { - /** - * List of concrete routes (absolute paths) that should be mounted at the application root. - */ - routes: AuthRoute[]; - - /** - * Convenience dispatcher that matches on `new URL(req.url).pathname` and calls the correct handler. - */ - handle: WebHandler; -}; - -const checkIssuerUrl = (issuer: URL): void => { - // Technically RFC 8414 does not permit a localhost HTTPS exemption, but this will be necessary for ease of testing - if (issuer.protocol !== 'https:' && issuer.hostname !== 'localhost' && issuer.hostname !== '127.0.0.1' && !allowInsecureIssuerUrl) { - throw new Error('Issuer URL must be HTTPS'); - } - if (issuer.hash) { - throw new Error(`Issuer URL must not have a fragment: ${issuer}`); - } - if (issuer.search) { - throw new Error(`Issuer URL must not have a query string: ${issuer}`); - } -}; - -export const createOAuthMetadata = (options: { - provider: OAuthServerProvider; - issuerUrl: URL; - baseUrl?: URL; - serviceDocumentationUrl?: URL; - scopesSupported?: string[]; -}): OAuthMetadata => { - const issuer = options.issuerUrl; - const baseUrl = options.baseUrl; - - checkIssuerUrl(issuer); - - const authorization_endpoint = '/authorize'; - const token_endpoint = '/token'; - const registration_endpoint = options.provider.clientsStore.registerClient ? '/register' : undefined; - const revocation_endpoint = options.provider.revokeToken ? '/revoke' : undefined; - - const metadata: OAuthMetadata = { - issuer: issuer.href, - service_documentation: options.serviceDocumentationUrl?.href, - - authorization_endpoint: new URL(authorization_endpoint, baseUrl || issuer).href, - response_types_supported: ['code'], - code_challenge_methods_supported: ['S256'], - - token_endpoint: new URL(token_endpoint, baseUrl || issuer).href, - token_endpoint_auth_methods_supported: ['client_secret_post', 'none'], - grant_types_supported: ['authorization_code', 'refresh_token'], - - scopes_supported: options.scopesSupported, - - revocation_endpoint: revocation_endpoint ? new URL(revocation_endpoint, baseUrl || issuer).href : undefined, - revocation_endpoint_auth_methods_supported: revocation_endpoint ? ['client_secret_post'] : undefined, - - registration_endpoint: registration_endpoint ? new URL(registration_endpoint, baseUrl || issuer).href : undefined - }; - - return metadata; -}; - -/** - * Installs standard MCP authorization server endpoints, including dynamic client registration and token revocation (if supported). - * Also advertises standard authorization server metadata, for easier discovery of supported configurations by clients. - * Note: if your MCP server is only a resource server and not an authorization server, use mcpAuthMetadataRouter instead. - */ -export function mcpAuthRouter(options: AuthRouterOptions): WebAuthRouter { - const oauthMetadata = createOAuthMetadata(options); - const routes: AuthRoute[] = []; - - routes.push({ - path: new URL(oauthMetadata.authorization_endpoint).pathname, - methods: ['GET', 'POST'], - handler: authorizationHandler({ provider: options.provider, ...options.authorizationOptions }) - }); - - routes.push({ - path: new URL(oauthMetadata.token_endpoint).pathname, - methods: ['POST', 'OPTIONS'], - handler: tokenHandler({ provider: options.provider, ...options.tokenOptions }) - }); - - const metadataRouter = mcpAuthMetadataRouter({ - oauthMetadata, - // Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat) - resourceServerUrl: options.resourceServerUrl ?? options.baseUrl ?? new URL(oauthMetadata.issuer), - serviceDocumentationUrl: options.serviceDocumentationUrl, - scopesSupported: options.scopesSupported, - resourceName: options.resourceName - }); - routes.push(...metadataRouter.routes); - - if (oauthMetadata.registration_endpoint) { - routes.push({ - path: new URL(oauthMetadata.registration_endpoint).pathname, - methods: ['POST', 'OPTIONS'], - handler: clientRegistrationHandler({ - clientsStore: options.provider.clientsStore, - ...options.clientRegistrationOptions - }) - }); - } - - if (oauthMetadata.revocation_endpoint) { - routes.push({ - path: new URL(oauthMetadata.revocation_endpoint).pathname, - methods: ['POST', 'OPTIONS'], - handler: revocationHandler({ provider: options.provider, ...options.revocationOptions }) - }); - } - - const handle: WebHandler = async (req, ctx) => { - const pathname = new URL(req.url).pathname; - const route = routes.find(r => r.path === pathname); - if (!route) { - return new Response('Not Found', { status: 404 }); - } - return route.handler(req, ctx); - }; - - return { routes, handle }; -} - -export type AuthMetadataOptions = { - /** - * OAuth Metadata as would be returned from the authorization server - * this MCP server relies on - */ - oauthMetadata: OAuthMetadata; - - /** - * The url of the MCP server, for use in protected resource metadata - */ - resourceServerUrl: URL; - - /** - * The url for documentation for the MCP server - */ - serviceDocumentationUrl?: URL; - - /** - * An optional list of scopes supported by this MCP server - */ - scopesSupported?: string[]; - - /** - * An optional resource name to display in resource metadata - */ - resourceName?: string; -}; - -export function mcpAuthMetadataRouter(options: AuthMetadataOptions): WebAuthRouter { - checkIssuerUrl(new URL(options.oauthMetadata.issuer)); - - const protectedResourceMetadata: OAuthProtectedResourceMetadata = { - resource: options.resourceServerUrl.href, - - authorization_servers: [options.oauthMetadata.issuer], - - scopes_supported: options.scopesSupported, - resource_name: options.resourceName, - resource_documentation: options.serviceDocumentationUrl?.href - }; - - // Serve PRM at the path-specific URL per RFC 9728 - const rsPath = new URL(options.resourceServerUrl.href).pathname; - const prmPath = `/.well-known/oauth-protected-resource${rsPath === '/' ? '' : rsPath}`; - - const routes: AuthRoute[] = [ - { path: prmPath, methods: ['GET', 'OPTIONS'], handler: metadataHandler(protectedResourceMetadata) }, - // Always add this for OAuth Authorization Server metadata per RFC 8414 - { path: '/.well-known/oauth-authorization-server', methods: ['GET', 'OPTIONS'], handler: metadataHandler(options.oauthMetadata) } - ]; - - const handle: WebHandler = async (req, ctx) => { - const pathname = new URL(req.url).pathname; - const route = routes.find(r => r.path === pathname); - if (!route) { - return new Response('Not Found', { status: 404 }); - } - return route.handler(req, ctx); - }; - - return { routes, handle }; -} - -/** - * Helper function to construct the OAuth 2.0 Protected Resource Metadata URL - * from a given server URL. This replaces the path with the standard metadata endpoint. - * - * @param serverUrl - The base URL of the protected resource server - * @returns The URL for the OAuth protected resource metadata endpoint - * - * @example - * getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com/mcp')) - * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource/mcp' - */ -export function getOAuthProtectedResourceMetadataUrl(serverUrl: URL): string { - const u = new URL(serverUrl.href); - const rsPath = u.pathname && u.pathname !== '/' ? u.pathname : ''; - return new URL(`/.well-known/oauth-protected-resource${rsPath}`, u).href; -} diff --git a/packages/server/src/server/auth/web.ts b/packages/server/src/server/auth/web.ts deleted file mode 100644 index e461e9711..000000000 --- a/packages/server/src/server/auth/web.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { MethodNotAllowedError } from '@modelcontextprotocol/core'; - -export type HeaderMap = Record; - -export type WebHandlerContext = { - /** - * Optional pre-parsed request body from an upstream framework. - * If provided, handlers will use this instead of reading from the Request stream. - */ - parsedBody?: unknown; -}; - -export type WebHandler = (req: Request, ctx?: WebHandlerContext) => Promise; - -export function jsonResponse(body: unknown, init?: { status?: number; headers?: HeaderMap }): Response { - const headers: HeaderMap = { 'Content-Type': 'application/json' }; - if (init?.headers) { - Object.assign(headers, init.headers); - } - return new Response(JSON.stringify(body), { - status: init?.status ?? 200, - headers - }); -} - -export function noStoreHeaders(): HeaderMap { - return { 'Cache-Control': 'no-store' }; -} - -export async function getParsedBody(req: Request, ctx?: WebHandlerContext): Promise { - if (ctx?.parsedBody !== undefined) { - return ctx.parsedBody; - } - - const ct = req.headers.get('content-type') ?? ''; - - if (ct.includes('application/json')) { - return await req.json(); - } - - if (ct.includes('application/x-www-form-urlencoded')) { - const text = await req.text(); - return objectFromUrlEncoded(text); - } - - // Empty bodies are treated as empty objects. - const text = await req.text(); - if (!text) return {}; - - // If content-type is missing/unknown, fall back to treating it as urlencoded-like. - return objectFromUrlEncoded(text); -} - -export function objectFromUrlEncoded(body: string): Record { - const params = new URLSearchParams(body); - const out: Record = {}; - for (const [k, v] of params.entries()) out[k] = v; - return out; -} - -export function methodNotAllowedResponse(req: Request, allowed: string[]): Response { - const error = new MethodNotAllowedError(`The method ${req.method} is not allowed for this endpoint`); - return jsonResponse(error.toResponseObject(), { - status: 405, - headers: { Allow: allowed.join(', ') } - }); -} - -export type CorsOptions = { - allowOrigin?: string; - allowMethods: readonly string[]; - allowHeaders?: readonly string[]; - exposeHeaders?: readonly string[]; - maxAgeSeconds?: number; -}; - -export function corsHeaders(options: CorsOptions): HeaderMap { - return { - 'Access-Control-Allow-Origin': options.allowOrigin ?? '*', - 'Access-Control-Allow-Methods': options.allowMethods.join(', '), - 'Access-Control-Allow-Headers': (options.allowHeaders ?? ['Content-Type', 'Authorization']).join(', '), - ...(options.exposeHeaders ? { 'Access-Control-Expose-Headers': options.exposeHeaders.join(', ') } : {}), - ...(options.maxAgeSeconds !== undefined ? { 'Access-Control-Max-Age': String(options.maxAgeSeconds) } : {}) - }; -} - -export function corsPreflightResponse(options: CorsOptions): Response { - return new Response(null, { - status: 204, - headers: corsHeaders(options) - }); -} diff --git a/packages/server/src/server/helper/body.ts b/packages/server/src/server/helper/body.ts new file mode 100644 index 000000000..7e08aaae8 --- /dev/null +++ b/packages/server/src/server/helper/body.ts @@ -0,0 +1,26 @@ +export async function getParsedBody(req: Request): Promise { + const ct = req.headers.get('content-type') ?? ''; + + if (ct.includes('application/json')) { + return await req.json(); + } + + if (ct.includes('application/x-www-form-urlencoded')) { + const text = await req.text(); + return objectFromUrlEncoded(text); + } + + // Empty bodies are treated as empty objects. + const text = await req.text(); + if (!text) return {}; + + // If content-type is missing/unknown, fall back to treating it as urlencoded-like. + return objectFromUrlEncoded(text); +} + +export function objectFromUrlEncoded(body: string): Record { + const params = new URLSearchParams(body); + const out: Record = {}; + for (const [k, v] of params.entries()) out[k] = v; + return out; +} diff --git a/packages/server/src/server/sse.ts b/packages/server/src/server/sse.ts deleted file mode 100644 index 06d418b2d..000000000 --- a/packages/server/src/server/sse.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import type { IncomingMessage, ServerResponse } from 'node:http'; -import { URL } from 'node:url'; - -import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestInfo, Transport } from '@modelcontextprotocol/core'; -import { JSONRPCMessageSchema } from '@modelcontextprotocol/core'; -import contentType from 'content-type'; -import getRawBody from 'raw-body'; - -const MAXIMUM_MESSAGE_SIZE = '4mb'; - -/** - * Configuration options for SSEServerTransport. - */ -export interface SSEServerTransportOptions { - /** - * List of allowed host header values for DNS rebinding protection. - * If not specified, host validation is disabled. - * @deprecated Use the `hostHeaderValidationResponse` helper from `@modelcontextprotocol/server`, - * or use `createMcpExpressApp` from `@modelcontextprotocol/server-express` which includes localhost protection by default. - */ - allowedHosts?: string[]; - - /** - * List of allowed origin header values for DNS rebinding protection. - * If not specified, origin validation is disabled. - * @deprecated Use the `hostHeaderValidationResponse` helper from `@modelcontextprotocol/server`, - * or use `createMcpExpressApp` from `@modelcontextprotocol/server-express` which includes localhost protection by default. - */ - allowedOrigins?: string[]; - - /** - * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). - * Default is false for backwards compatibility. - * @deprecated Use the `hostHeaderValidationResponse` helper from `@modelcontextprotocol/server`, - * or use `createMcpExpressApp` from `@modelcontextprotocol/server-express` which includes localhost protection by default. - */ - enableDnsRebindingProtection?: boolean; -} - -/** - * Server transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests. - * - * This transport is only available in Node.js environments. - * @deprecated SSEServerTransport is deprecated. Use NodeStreamableHTTPServerTransport instead. - */ -export class SSEServerTransport implements Transport { - private _sseResponse?: ServerResponse; - private _sessionId: string; - private _options: SSEServerTransportOptions; - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; - - /** - * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`. - */ - constructor( - private _endpoint: string, - private res: ServerResponse, - options?: SSEServerTransportOptions - ) { - this._sessionId = randomUUID(); - this._options = options || { enableDnsRebindingProtection: false }; - } - - /** - * Validates request headers for DNS rebinding protection. - * @returns Error message if validation fails, undefined if validation passes. - */ - private validateRequestHeaders(req: IncomingMessage): string | undefined { - // Skip validation if protection is not enabled - if (!this._options.enableDnsRebindingProtection) { - return undefined; - } - - // Validate Host header if allowedHosts is configured - if (this._options.allowedHosts && this._options.allowedHosts.length > 0) { - const hostHeader = req.headers.host; - if (!hostHeader || !this._options.allowedHosts.includes(hostHeader)) { - return `Invalid Host header: ${hostHeader}`; - } - } - - // Validate Origin header if allowedOrigins is configured - if (this._options.allowedOrigins && this._options.allowedOrigins.length > 0) { - const originHeader = req.headers.origin; - if (originHeader && !this._options.allowedOrigins.includes(originHeader)) { - return `Invalid Origin header: ${originHeader}`; - } - } - - return undefined; - } - - /** - * Handles the initial SSE connection request. - * - * This should be called when a GET request is made to establish the SSE stream. - */ - async start(): Promise { - if (this._sseResponse) { - throw new Error('SSEServerTransport already started! If using Server class, note that connect() calls start() automatically.'); - } - - this.res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache, no-transform', - Connection: 'keep-alive' - }); - - // Send the endpoint event - // Use a dummy base URL because this._endpoint is relative. - // This allows using URL/URLSearchParams for robust parameter handling. - const dummyBase = 'http://localhost'; // Any valid base works - const endpointUrl = new URL(this._endpoint, dummyBase); - endpointUrl.searchParams.set('sessionId', this._sessionId); - - // Reconstruct the relative URL string (pathname + search + hash) - const relativeUrlWithSession = endpointUrl.pathname + endpointUrl.search + endpointUrl.hash; - - this.res.write(`event: endpoint\ndata: ${relativeUrlWithSession}\n\n`); - - this._sseResponse = this.res; - this.res.on('close', () => { - this._sseResponse = undefined; - this.onclose?.(); - }); - } - - /** - * Handles incoming POST messages. - * - * This should be called when a POST request is made to send a message to the server. - */ - async handlePostMessage(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise { - if (!this._sseResponse) { - const message = 'SSE connection not established'; - res.writeHead(500).end(message); - throw new Error(message); - } - - // Validate request headers for DNS rebinding protection - const validationError = this.validateRequestHeaders(req); - if (validationError) { - res.writeHead(403).end(validationError); - this.onerror?.(new Error(validationError)); - return; - } - - const authInfo: AuthInfo | undefined = req.auth; - const requestInfo: RequestInfo = { headers: req.headers }; - - let body: string | unknown; - try { - const ct = contentType.parse(req.headers['content-type'] ?? ''); - if (ct.type !== 'application/json') { - throw new Error(`Unsupported content-type: ${ct.type}`); - } - - body = - parsedBody ?? - (await getRawBody(req, { - limit: MAXIMUM_MESSAGE_SIZE, - encoding: ct.parameters.charset ?? 'utf-8' - })); - } catch (error) { - res.writeHead(400).end(String(error)); - this.onerror?.(error as Error); - return; - } - - try { - await this.handleMessage(typeof body === 'string' ? JSON.parse(body) : body, { requestInfo, authInfo }); - } catch { - res.writeHead(400).end(`Invalid message: ${body}`); - return; - } - - res.writeHead(202).end('Accepted'); - } - - /** - * Handle a client message, regardless of how it arrived. This can be used to inform the server of messages that arrive via a means different than HTTP POST. - */ - async handleMessage(message: unknown, extra?: MessageExtraInfo): Promise { - let parsedMessage: JSONRPCMessage; - try { - parsedMessage = JSONRPCMessageSchema.parse(message); - } catch (error) { - this.onerror?.(error as Error); - throw error; - } - - this.onmessage?.(parsedMessage, extra); - } - - async close(): Promise { - this._sseResponse?.end(); - this._sseResponse = undefined; - this.onclose?.(); - } - - async send(message: JSONRPCMessage): Promise { - if (!this._sseResponse) { - throw new Error('Not connected'); - } - - this._sseResponse.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`); - } - - /** - * Returns the session ID for this transport. - * - * This can be used to route incoming POST requests. - */ - get sessionId(): string { - return this._sessionId; - } -} diff --git a/packages/server/test/server/auth/handlers/authorize.test.ts b/packages/server/test/server/auth/handlers/authorize.test.ts deleted file mode 100644 index c5943915c..000000000 --- a/packages/server/test/server/auth/handlers/authorize.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; - -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import { authorizationHandler } from '../../../../src/server/auth/handlers/authorize.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; - -describe('authorizationHandler (web)', () => { - const validClient: OAuthClientInformationFull = { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - - const multiRedirectClient: OAuthClientInformationFull = { - client_id: 'multi-redirect-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback1', 'https://example.com/callback2'] - }; - - const clientsStore: OAuthRegisteredClientsStore = { - async getClient(clientId: string) { - if (clientId === 'valid-client') return validClient; - if (clientId === 'multi-redirect-client') return multiRedirectClient; - return undefined; - } - }; - - const provider: OAuthServerProvider = { - clientsStore, - async authorize(_client, params: AuthorizationParams): Promise { - const u = new URL(params.redirectUri); - u.searchParams.set('code', 'mock_auth_code'); - if (params.state) u.searchParams.set('state', params.state); - return Response.redirect(u.toString(), 302); - }, - async challengeForAuthorizationCode(): Promise { - return 'mock_challenge'; - }, - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - async verifyAccessToken() { - throw new Error('not used'); - } - }; - - it('returns 405 for unsupported methods', async () => { - const handler = authorizationHandler({ provider }); - const res = await handler(new Request('http://localhost/authorize', { method: 'PUT' })); - expect(res.status).toBe(405); - }); - - it('returns 400 if client does not exist', async () => { - const handler = authorizationHandler({ provider }); - const res = await handler( - new Request( - 'http://localhost/authorize?client_id=missing&response_type=code&code_challenge=x&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback', - { method: 'GET' } - ) - ); - expect(res.status).toBe(400); - expect(await res.json()).toEqual(expect.objectContaining({ error: 'invalid_client' })); - }); - - it('redirects with a code on valid request (single redirect_uri inferred)', async () => { - const handler = authorizationHandler({ provider }); - const res = await handler( - new Request( - 'http://localhost/authorize?client_id=valid-client&response_type=code&code_challenge=challenge123&code_challenge_method=S256', - { method: 'GET' } - ) - ); - expect(res.status).toBe(302); - const location = res.headers.get('location')!; - expect(location).toContain('https://example.com/callback'); - expect(location).toContain('code=mock_auth_code'); - expect(res.headers.get('cache-control')).toBe('no-store'); - }); - - it('requires redirect_uri if client has multiple redirect URIs', async () => { - const handler = authorizationHandler({ provider }); - const res = await handler( - new Request( - 'http://localhost/authorize?client_id=multi-redirect-client&response_type=code&code_challenge=challenge123&code_challenge_method=S256', - { method: 'GET' } - ) - ); - expect(res.status).toBe(400); - expect(await res.json()).toEqual(expect.objectContaining({ error: 'invalid_request' })); - }); -}); diff --git a/packages/server/test/server/auth/handlers/metadata.test.ts b/packages/server/test/server/auth/handlers/metadata.test.ts deleted file mode 100644 index 722320925..000000000 --- a/packages/server/test/server/auth/handlers/metadata.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { OAuthMetadata } from '@modelcontextprotocol/core'; - -import { metadataHandler } from '../../../../src/server/auth/handlers/metadata.js'; - -describe('Metadata Handler', () => { - const exampleMetadata: OAuthMetadata = { - issuer: 'https://auth.example.com', - authorization_endpoint: 'https://auth.example.com/authorize', - token_endpoint: 'https://auth.example.com/token', - registration_endpoint: 'https://auth.example.com/register', - revocation_endpoint: 'https://auth.example.com/revoke', - scopes_supported: ['profile', 'email'], - response_types_supported: ['code'], - grant_types_supported: ['authorization_code', 'refresh_token'], - token_endpoint_auth_methods_supported: ['client_secret_basic'], - code_challenge_methods_supported: ['S256'] - }; - - it('requires GET method', async () => { - const handler = metadataHandler(exampleMetadata); - const res = await handler(new Request('http://localhost/.well-known/oauth-authorization-server', { method: 'POST' })); - - expect(res.status).toBe(405); - expect(res.headers.get('allow')).toBe('GET, OPTIONS'); - expect(await res.json()).toEqual({ - error: 'method_not_allowed', - error_description: 'The method POST is not allowed for this endpoint' - }); - }); - - it('returns the metadata object', async () => { - const handler = metadataHandler(exampleMetadata); - const res = await handler(new Request('http://localhost/.well-known/oauth-authorization-server', { method: 'GET' })); - - expect(res.status).toBe(200); - expect(await res.json()).toEqual(exampleMetadata); - }); - - it('includes CORS headers in response', async () => { - const handler = metadataHandler(exampleMetadata); - const res = await handler( - new Request('http://localhost/.well-known/oauth-authorization-server', { - method: 'GET', - headers: { Origin: 'https://example.com' } - }) - ); - - expect(res.headers.get('access-control-allow-origin')).toBe('*'); - }); - - it('supports OPTIONS preflight requests', async () => { - const handler = metadataHandler(exampleMetadata); - const res = await handler( - new Request('http://localhost/.well-known/oauth-authorization-server', { - method: 'OPTIONS', - headers: { - Origin: 'https://example.com', - 'Access-Control-Request-Method': 'GET' - } - }) - ); - - expect(res.status).toBe(204); - expect(res.headers.get('access-control-allow-origin')).toBe('*'); - }); - - it('works with minimal metadata', async () => { - const minimalMetadata: OAuthMetadata = { - issuer: 'https://auth.example.com', - authorization_endpoint: 'https://auth.example.com/authorize', - token_endpoint: 'https://auth.example.com/token', - response_types_supported: ['code'] - }; - const handler = metadataHandler(minimalMetadata); - const res = await handler(new Request('http://localhost/.well-known/oauth-authorization-server', { method: 'GET' })); - - expect(res.status).toBe(200); - expect(await res.json()).toEqual(minimalMetadata); - }); -}); diff --git a/packages/server/test/server/auth/handlers/register.test.ts b/packages/server/test/server/auth/handlers/register.test.ts deleted file mode 100644 index 6a2ffcd11..000000000 --- a/packages/server/test/server/auth/handlers/register.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; - -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import { clientRegistrationHandler } from '../../../../src/server/auth/handlers/register.js'; - -describe('clientRegistrationHandler (web)', () => { - it('returns 201 and client info when registration is supported', async () => { - const clientsStore: OAuthRegisteredClientsStore = { - async getClient() { - return undefined; - }, - async registerClient(client: Omit) { - // In real implementation, server may generate ids; here return minimal. - return { - ...client, - client_id: 'generated-client', - client_id_issued_at: Math.floor(Date.now() / 1000), - redirect_uris: (client as any).redirect_uris ?? [] - } as unknown as OAuthClientInformationFull; - } - }; - - const handler = clientRegistrationHandler({ clientsStore }); - - const res = await handler( - new Request('http://localhost/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - redirect_uris: ['https://example.com/callback'] - }) - }) - ); - - expect(res.status).toBe(201); - const body = (await res.json()) as { client_id?: string }; - expect(body.client_id).toBeDefined(); - }); -}); diff --git a/packages/server/test/server/auth/handlers/revoke.test.ts b/packages/server/test/server/auth/handlers/revoke.test.ts deleted file mode 100644 index d960f26c3..000000000 --- a/packages/server/test/server/auth/handlers/revoke.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; - -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import { revocationHandler } from '../../../../src/server/auth/handlers/revoke.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; - -describe('revocationHandler (web)', () => { - it('returns 200 on successful revocation', async () => { - const validClient: OAuthClientInformationFull = { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - - const clientsStore: OAuthRegisteredClientsStore = { - async getClient(clientId: string) { - return clientId === 'valid-client' ? validClient : undefined; - } - }; - - const provider: OAuthServerProvider = { - clientsStore, - async authorize(_client: OAuthClientInformationFull, _params: AuthorizationParams): Promise { - return Response.redirect('https://example.com', 302); - }, - async challengeForAuthorizationCode(): Promise { - return 'mock'; - }, - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - async verifyAccessToken() { - throw new Error('not used'); - }, - async revokeToken(_client: OAuthClientInformationFull, _request: OAuthTokenRevocationRequest): Promise { - // ok - } - }; - - const handler = revocationHandler({ provider }); - - const body = new URLSearchParams({ - client_id: 'valid-client', - client_secret: 'valid-secret', - token: 'token_to_revoke' - }).toString(); - - const res = await handler( - new Request('http://localhost/revoke', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body - }) - ); - - expect(res.status).toBe(200); - expect(await res.json()).toEqual({}); - }); -}); diff --git a/packages/server/test/server/auth/handlers/token.test.ts b/packages/server/test/server/auth/handlers/token.test.ts deleted file mode 100644 index d99e4b39a..000000000 --- a/packages/server/test/server/auth/handlers/token.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { AuthInfo, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; -import { InvalidGrantError } from '@modelcontextprotocol/core'; -import * as pkceChallenge from 'pkce-challenge'; - -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import { tokenHandler } from '../../../../src/server/auth/handlers/token.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; - -vi.mock('pkce-challenge', () => ({ - verifyChallenge: vi.fn() -})); - -describe('tokenHandler (web)', () => { - const validClient: OAuthClientInformationFull = { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - - const clientsStore: OAuthRegisteredClientsStore = { - async getClient(clientId: string) { - return clientId === 'valid-client' ? validClient : undefined; - } - }; - - const provider: OAuthServerProvider = { - clientsStore, - async authorize(_client: OAuthClientInformationFull, _params: AuthorizationParams): Promise { - return Response.redirect('https://example.com/callback?code=mock_auth_code', 302); - }, - async challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise { - if (authorizationCode === 'valid_code') return 'mock_challenge'; - throw new InvalidGrantError('The authorization code is invalid'); - }, - async exchangeAuthorizationCode(): Promise { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; - }, - async exchangeRefreshToken(): Promise { - return { - access_token: 'new_mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'new_mock_refresh_token' - }; - }, - async verifyAccessToken(token: string): Promise { - return { - token, - clientId: 'valid-client', - scopes: [], - expiresAt: Math.floor(Date.now() / 1000) + 3600 - }; - } - }; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('returns tokens for authorization_code grant when PKCE passes', async () => { - (pkceChallenge.verifyChallenge as unknown as ReturnType).mockResolvedValue(true); - const handler = tokenHandler({ provider }); - - const body = new URLSearchParams({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'valid_verifier' - }).toString(); - - const res = await handler( - new Request('http://localhost/token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body - }) - ); - - expect(res.status).toBe(200); - expect(await res.json()).toEqual( - expect.objectContaining({ - access_token: 'mock_access_token' - }) - ); - }); - - it('returns 400 when PKCE fails', async () => { - (pkceChallenge.verifyChallenge as unknown as ReturnType).mockResolvedValue(false); - const handler = tokenHandler({ provider }); - - const body = new URLSearchParams({ - client_id: 'valid-client', - client_secret: 'valid-secret', - grant_type: 'authorization_code', - code: 'valid_code', - code_verifier: 'bad_verifier' - }).toString(); - - const res = await handler( - new Request('http://localhost/token', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body - }) - ); - - expect(res.status).toBe(400); - expect(await res.json()).toEqual(expect.objectContaining({ error: 'invalid_grant' })); - }); -}); diff --git a/packages/server/test/server/auth/middleware/allowedMethods.test.ts b/packages/server/test/server/auth/middleware/allowedMethods.test.ts deleted file mode 100644 index 3cea847a1..000000000 --- a/packages/server/test/server/auth/middleware/allowedMethods.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { allowedMethods } from '../../../../src/server/auth/middleware/allowedMethods.js'; - -describe('allowedMethods', () => { - test('returns undefined for allowed HTTP method', () => { - const req = new Request('http://localhost/test', { method: 'GET' }); - const res = allowedMethods(['GET'], req); - expect(res).toBeUndefined(); - }); - - test('returns 405 response for disallowed HTTP method', async () => { - const req = new Request('http://localhost/test', { method: 'POST' }); - const res = allowedMethods(['GET'], req); - expect(res).toBeDefined(); - expect(res!.status).toBe(405); - expect(res!.headers.get('allow')).toBe('GET'); - expect(await res!.json()).toEqual({ - error: 'method_not_allowed', - error_description: 'The method POST is not allowed for this endpoint' - }); - }); - - test('supports multiple allowed methods', async () => { - const req = new Request('http://localhost/test', { method: 'PUT' }); - const res = allowedMethods(['GET', 'POST'], req); - expect(res).toBeDefined(); - expect(res!.status).toBe(405); - expect(res!.headers.get('allow')).toBe('GET, POST'); - }); -}); diff --git a/packages/server/test/server/auth/middleware/bearerAuth.test.ts b/packages/server/test/server/auth/middleware/bearerAuth.test.ts deleted file mode 100644 index 9b0ead3ef..000000000 --- a/packages/server/test/server/auth/middleware/bearerAuth.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { AuthInfo } from '@modelcontextprotocol/core'; -import { InsufficientScopeError, InvalidTokenError, ServerError } from '@modelcontextprotocol/core'; - -import { requireBearerAuth } from '../../../../src/server/auth/middleware/bearerAuth.js'; -import type { OAuthTokenVerifier } from '../../../../src/server/auth/provider.js'; - -describe('requireBearerAuth (web)', () => { - const verifyAccessToken = vi.fn(); - const verifier: OAuthTokenVerifier = { verifyAccessToken }; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('returns authInfo on success', async () => { - const info: AuthInfo = { - token: 't', - clientId: 'c', - scopes: ['read'], - expiresAt: Math.floor(Date.now() / 1000) + 3600 - }; - verifyAccessToken.mockResolvedValue(info); - - const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); - const result = await requireBearerAuth(req, { verifier }); - - expect('authInfo' in result).toBe(true); - if ('authInfo' in result) { - expect(result.authInfo).toEqual(info); - } - }); - - it('returns 401 when missing Authorization header', async () => { - const req = new Request('http://localhost/x'); - const result = await requireBearerAuth(req, { verifier }); - - expect('response' in result).toBe(true); - if ('response' in result) { - expect(result.response.status).toBe(401); - expect(result.response.headers.get('www-authenticate')).toContain('Bearer error="invalid_token"'); - expect(await result.response.json()).toEqual( - expect.objectContaining({ error: 'invalid_token', error_description: 'Missing Authorization header' }) - ); - } - }); - - it('returns 401 when verifier throws InvalidTokenError', async () => { - verifyAccessToken.mockRejectedValue(new InvalidTokenError('bad')); - const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); - const result = await requireBearerAuth(req, { verifier }); - - expect('response' in result).toBe(true); - if ('response' in result) { - expect(result.response.status).toBe(401); - } - }); - - it('returns 403 when scopes are insufficient', async () => { - const info: AuthInfo = { - token: 't', - clientId: 'c', - scopes: ['read'], - expiresAt: Math.floor(Date.now() / 1000) + 3600 - }; - verifyAccessToken.mockResolvedValue(info); - - const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); - const result = await requireBearerAuth(req, { verifier, requiredScopes: ['read', 'write'] }); - - expect('response' in result).toBe(true); - if ('response' in result) { - expect(result.response.status).toBe(403); - expect(result.response.headers.get('www-authenticate')).toContain('Bearer error="insufficient_scope"'); - expect(await result.response.json()).toEqual( - expect.objectContaining({ error: 'insufficient_scope', error_description: 'Insufficient scope' }) - ); - } - }); - - it('returns 500 when verifier throws ServerError', async () => { - verifyAccessToken.mockRejectedValue(new ServerError('boom')); - const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); - const result = await requireBearerAuth(req, { verifier }); - - expect('response' in result).toBe(true); - if ('response' in result) { - expect(result.response.status).toBe(500); - } - }); - - it('includes scope and resource_metadata in WWW-Authenticate when provided', async () => { - verifyAccessToken.mockRejectedValue(new InvalidTokenError('bad')); - const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); - const result = await requireBearerAuth(req, { - verifier, - requiredScopes: ['read', 'write'], - resourceMetadataUrl: 'https://example.com/.well-known/oauth-protected-resource' - }); - - expect('response' in result).toBe(true); - if ('response' in result) { - const header = result.response.headers.get('www-authenticate') ?? ''; - expect(header).toContain('scope="read write"'); - expect(header).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"'); - } - }); - - it('passes through InsufficientScopeError from verifier as 403', async () => { - verifyAccessToken.mockRejectedValue(new InsufficientScopeError('nope')); - const req = new Request('http://localhost/x', { headers: { Authorization: 'Bearer t' } }); - const result = await requireBearerAuth(req, { verifier }); - - expect('response' in result).toBe(true); - if ('response' in result) { - expect(result.response.status).toBe(403); - } - }); -}); diff --git a/packages/server/test/server/auth/middleware/clientAuth.test.ts b/packages/server/test/server/auth/middleware/clientAuth.test.ts deleted file mode 100644 index 0ee9aae0a..000000000 --- a/packages/server/test/server/auth/middleware/clientAuth.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import { InvalidClientError, InvalidRequestError } from '@modelcontextprotocol/core'; - -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { ClientAuthenticationMiddlewareOptions } from '../../../../src/server/auth/middleware/clientAuth.js'; -import { authenticateClient } from '../../../../src/server/auth/middleware/clientAuth.js'; - -describe('authenticateClient', () => { - // Mock client store - const mockClientStore: OAuthRegisteredClientsStore = { - async getClient(clientId: string): Promise { - if (clientId === 'valid-client') { - return { - client_id: 'valid-client', - client_secret: 'valid-secret', - redirect_uris: ['https://example.com/callback'] - }; - } else if (clientId === 'expired-client') { - // Client with no secret - return { - client_id: 'expired-client', - redirect_uris: ['https://example.com/callback'] - }; - } else if (clientId === 'client-with-expired-secret') { - // Client with an expired secret - return { - client_id: 'client-with-expired-secret', - client_secret: 'expired-secret', - client_secret_expires_at: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago - redirect_uris: ['https://example.com/callback'] - }; - } - return undefined; - } - }; - - let options: ClientAuthenticationMiddlewareOptions; - - beforeEach(() => { - options = { - clientsStore: mockClientStore - }; - }); - - it('authenticates valid client credentials', async () => { - const client = await authenticateClient( - { - client_id: 'valid-client', - client_secret: 'valid-secret' - }, - options - ); - - expect(client.client_id).toBe('valid-client'); - }); - - it('rejects invalid client_id', async () => { - await expect( - authenticateClient( - { - client_id: 'non-existent-client', - client_secret: 'some-secret' - }, - options - ) - ).rejects.toBeInstanceOf(InvalidClientError); - }); - - it('rejects invalid client_secret', async () => { - await expect( - authenticateClient( - { - client_id: 'valid-client', - client_secret: 'wrong-secret' - }, - options - ) - ).rejects.toBeInstanceOf(InvalidClientError); - }); - - it('rejects missing client_id', async () => { - await expect( - authenticateClient( - { - client_secret: 'valid-secret' - }, - options - ) - ).rejects.toBeInstanceOf(InvalidRequestError); - }); - - it('allows missing client_secret if client has none', async () => { - const client = await authenticateClient( - { - client_id: 'expired-client' - }, - options - ); - expect(client.client_id).toBe('expired-client'); - }); - - it('rejects request when client secret has expired', async () => { - await expect( - authenticateClient( - { - client_id: 'client-with-expired-secret', - client_secret: 'expired-secret' - }, - options - ) - ).rejects.toBeInstanceOf(InvalidClientError); - }); - - it('ignores extra fields in request', async () => { - const client = await authenticateClient( - { - client_id: 'valid-client', - client_secret: 'valid-secret', - extra_field: 'ignored' - }, - options - ); - expect(client.client_id).toBe('valid-client'); - }); -}); diff --git a/packages/server/test/server/auth/providers/proxyProvider.test.ts b/packages/server/test/server/auth/providers/proxyProvider.test.ts deleted file mode 100644 index 143cfa78d..000000000 --- a/packages/server/test/server/auth/providers/proxyProvider.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -import type { AuthInfo, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; -import { InsufficientScopeError, InvalidTokenError, ServerError } from '@modelcontextprotocol/core'; -import { type Mock } from 'vitest'; - -import type { ProxyOptions } from '../../../../src/server/auth/providers/proxyProvider.js'; -import { ProxyOAuthServerProvider } from '../../../../src/server/auth/providers/proxyProvider.js'; - -describe('Proxy OAuth Server Provider', () => { - // Mock client data - const validClient: OAuthClientInformationFull = { - client_id: 'test-client', - client_secret: 'test-secret', - redirect_uris: ['https://example.com/callback'] - }; - - // Mock provider functions - const mockVerifyToken = vi.fn(); - const mockGetClient = vi.fn(); - - // Base provider options - const baseOptions: ProxyOptions = { - endpoints: { - authorizationUrl: 'https://auth.example.com/authorize', - tokenUrl: 'https://auth.example.com/token', - revocationUrl: 'https://auth.example.com/revoke', - registrationUrl: 'https://auth.example.com/register' - }, - verifyAccessToken: mockVerifyToken, - getClient: mockGetClient - }; - - let provider: ProxyOAuthServerProvider; - let originalFetch: typeof global.fetch; - - beforeEach(() => { - provider = new ProxyOAuthServerProvider(baseOptions); - originalFetch = global.fetch; - global.fetch = vi.fn(); - - // Setup mock implementations - mockVerifyToken.mockImplementation(async (token: string) => { - if (token === 'valid-token') { - return { - token, - clientId: 'test-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 - } as AuthInfo; - } - throw new InvalidTokenError('Invalid token'); - }); - - mockGetClient.mockImplementation(async (clientId: string) => { - if (clientId === 'test-client') { - return validClient; - } - return undefined; - }); - }); - - // Add helper function for failed responses - const mockFailedResponse = () => { - (global.fetch as Mock).mockImplementation(() => - Promise.resolve({ - ok: false, - status: 400 - }) - ); - }; - - afterEach(() => { - global.fetch = originalFetch; - vi.clearAllMocks(); - }); - - describe('authorization', () => { - it('redirects to authorization endpoint with correct parameters', async () => { - const response = await provider.authorize(validClient, { - redirectUri: 'https://example.com/callback', - codeChallenge: 'test-challenge', - state: 'test-state', - scopes: ['read', 'write'], - resource: new URL('https://api.example.com/resource') - }); - - const expectedUrl = new URL('https://auth.example.com/authorize'); - expectedUrl.searchParams.set('client_id', 'test-client'); - expectedUrl.searchParams.set('response_type', 'code'); - expectedUrl.searchParams.set('redirect_uri', 'https://example.com/callback'); - expectedUrl.searchParams.set('code_challenge', 'test-challenge'); - expectedUrl.searchParams.set('code_challenge_method', 'S256'); - expectedUrl.searchParams.set('state', 'test-state'); - expectedUrl.searchParams.set('scope', 'read write'); - expectedUrl.searchParams.set('resource', 'https://api.example.com/resource'); - - expect(response.status).toBe(302); - expect(response.headers.get('location')).toBe(expectedUrl.toString()); - }); - }); - - describe('token exchange', () => { - const mockTokenResponse: OAuthTokens = { - access_token: 'new-access-token', - token_type: 'Bearer', - expires_in: 3600, - refresh_token: 'new-refresh-token' - }; - - beforeEach(() => { - (global.fetch as Mock).mockImplementation(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockTokenResponse) - }) - ); - }); - - it('exchanges authorization code for tokens', async () => { - const tokens = await provider.exchangeAuthorizationCode(validClient, 'test-code', 'test-verifier'); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://auth.example.com/token', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: expect.stringContaining('grant_type=authorization_code') - }) - ); - expect(tokens).toEqual(mockTokenResponse); - }); - - it('includes redirect_uri in token request when provided', async () => { - const redirectUri = 'https://example.com/callback'; - const tokens = await provider.exchangeAuthorizationCode(validClient, 'test-code', 'test-verifier', redirectUri); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://auth.example.com/token', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: expect.stringContaining(`redirect_uri=${encodeURIComponent(redirectUri)}`) - }) - ); - expect(tokens).toEqual(mockTokenResponse); - }); - - it('includes resource parameter in authorization code exchange', async () => { - const tokens = await provider.exchangeAuthorizationCode( - validClient, - 'test-code', - 'test-verifier', - 'https://example.com/callback', - new URL('https://api.example.com/resource') - ); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://auth.example.com/token', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: expect.stringContaining('resource=' + encodeURIComponent('https://api.example.com/resource')) - }) - ); - expect(tokens).toEqual(mockTokenResponse); - }); - - it('handles authorization code exchange without resource parameter', async () => { - const tokens = await provider.exchangeAuthorizationCode(validClient, 'test-code', 'test-verifier'); - - const fetchCall = (global.fetch as Mock).mock.calls[0]; - const body = fetchCall![1].body as string; - expect(body).not.toContain('resource='); - expect(tokens).toEqual(mockTokenResponse); - }); - - it('exchanges refresh token for new tokens', async () => { - const tokens = await provider.exchangeRefreshToken(validClient, 'test-refresh-token', ['read', 'write']); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://auth.example.com/token', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: expect.stringContaining('grant_type=refresh_token') - }) - ); - expect(tokens).toEqual(mockTokenResponse); - }); - - it('includes resource parameter in refresh token exchange', async () => { - const tokens = await provider.exchangeRefreshToken( - validClient, - 'test-refresh-token', - ['read', 'write'], - new URL('https://api.example.com/resource') - ); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://auth.example.com/token', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: expect.stringContaining('resource=' + encodeURIComponent('https://api.example.com/resource')) - }) - ); - expect(tokens).toEqual(mockTokenResponse); - }); - }); - - describe('client registration', () => { - it('registers new client', async () => { - const newClient: OAuthClientInformationFull = { - client_id: 'new-client', - redirect_uris: ['https://new-client.com/callback'] - }; - - (global.fetch as Mock).mockImplementation(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(newClient) - }) - ); - - const result = await provider.clientsStore.registerClient!(newClient); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://auth.example.com/register', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(newClient) - }) - ); - expect(result).toEqual(newClient); - }); - - it('handles registration failure', async () => { - mockFailedResponse(); - const newClient: OAuthClientInformationFull = { - client_id: 'new-client', - redirect_uris: ['https://new-client.com/callback'] - }; - - await expect(provider.clientsStore.registerClient!(newClient)).rejects.toThrow(ServerError); - }); - }); - - describe('token revocation', () => { - it('revokes token', async () => { - (global.fetch as Mock).mockImplementation(() => - Promise.resolve({ - ok: true - }) - ); - - await provider.revokeToken!(validClient, { - token: 'token-to-revoke', - token_type_hint: 'access_token' - }); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://auth.example.com/revoke', - expect.objectContaining({ - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: expect.stringContaining('token=token-to-revoke') - }) - ); - }); - - it('handles revocation failure', async () => { - mockFailedResponse(); - await expect( - provider.revokeToken!(validClient, { - token: 'invalid-token' - }) - ).rejects.toThrow(ServerError); - }); - }); - - describe('token verification', () => { - it('verifies valid token', async () => { - const validAuthInfo: AuthInfo = { - token: 'valid-token', - clientId: 'test-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 - }; - mockVerifyToken.mockResolvedValue(validAuthInfo); - - const authInfo = await provider.verifyAccessToken('valid-token'); - expect(authInfo).toEqual(validAuthInfo); - expect(mockVerifyToken).toHaveBeenCalledWith('valid-token'); - }); - - it('passes through InvalidTokenError', async () => { - const error = new InvalidTokenError('Token expired'); - mockVerifyToken.mockRejectedValue(error); - - await expect(provider.verifyAccessToken('invalid-token')).rejects.toBe(error); - expect(mockVerifyToken).toHaveBeenCalledWith('invalid-token'); - }); - - it('passes through InsufficientScopeError', async () => { - const error = new InsufficientScopeError('Required scopes: read, write'); - mockVerifyToken.mockRejectedValue(error); - - await expect(provider.verifyAccessToken('token-with-insufficient-scope')).rejects.toBe(error); - expect(mockVerifyToken).toHaveBeenCalledWith('token-with-insufficient-scope'); - }); - - it('passes through unexpected errors', async () => { - const error = new Error('Unexpected error'); - mockVerifyToken.mockRejectedValue(error); - - await expect(provider.verifyAccessToken('valid-token')).rejects.toBe(error); - expect(mockVerifyToken).toHaveBeenCalledWith('valid-token'); - }); - }); -}); diff --git a/packages/server/test/server/sse.test.ts b/packages/server/test/server/sse.test.ts deleted file mode 100644 index 0fc9eebc8..000000000 --- a/packages/server/test/server/sse.test.ts +++ /dev/null @@ -1,733 +0,0 @@ -import type http from 'node:http'; -import { createServer, type Server } from 'node:http'; - -import type { CallToolResult, JSONRPCMessage } from '@modelcontextprotocol/core'; -import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; -import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; -import { type Mocked } from 'vitest'; - -import { McpServer } from '../../src/server/mcp.js'; -import { SSEServerTransport } from '../../src/server/sse.js'; - -const createMockResponse = () => { - const res = { - writeHead: vi.fn().mockReturnThis(), - write: vi.fn().mockReturnThis(), - on: vi.fn().mockReturnThis(), - end: vi.fn().mockReturnThis() - }; - - return res as unknown as Mocked; -}; - -const createMockRequest = ({ headers = {}, body }: { headers?: Record; body?: string } = {}) => { - const mockReq = { - headers, - body: body ? body : undefined, - auth: { - token: 'test-token' - }, - on: vi.fn().mockImplementation((event, listener) => { - const mockListener = listener as unknown as (...args: unknown[]) => void; - if (event === 'data') { - mockListener(Buffer.from(body || '') as unknown as Error); - } - if (event === 'error') { - mockListener(new Error('test')); - } - if (event === 'end') { - mockListener(); - } - if (event === 'close') { - setTimeout(listener, 100); - } - return mockReq; - }), - listeners: vi.fn(), - removeListener: vi.fn() - } as unknown as http.IncomingMessage; - - return mockReq; -}; - -async function readAllSSEEvents(response: Response): Promise { - const reader = response.body?.getReader(); - if (!reader) throw new Error('No readable stream'); - - const events: string[] = []; - const decoder = new TextDecoder(); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - if (value) { - events.push(decoder.decode(value)); - } - } - } finally { - reader.releaseLock(); - } - - return events; -} - -/** - * Helper to send JSON-RPC request - */ -async function sendSsePostRequest( - baseUrl: URL, - message: JSONRPCMessage | JSONRPCMessage[], - sessionId?: string, - extraHeaders?: Record -): Promise { - const headers: Record = { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - ...extraHeaders - }; - - if (sessionId) { - baseUrl.searchParams.set('sessionId', sessionId); - } - - return fetch(baseUrl, { - method: 'POST', - headers, - body: JSON.stringify(message) - }); -} - -describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { - const { z } = entry; - - /** - * Helper to create and start test HTTP server with MCP setup - */ - async function createTestServerWithSse(args: { mockRes: http.ServerResponse }): Promise<{ - server: Server; - transport: SSEServerTransport; - mcpServer: McpServer; - baseUrl: URL; - sessionId: string; - serverPort: number; - }> { - const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); - - mcpServer.tool( - 'greet', - 'A simple greeting tool', - { name: z.string().describe('Name to greet') }, - async ({ name }): Promise => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; - } - ); - - const endpoint = '/messages'; - - const transport = new SSEServerTransport(endpoint, args.mockRes); - const sessionId = transport.sessionId; - - await mcpServer.connect(transport); - - const server = createServer(async (req, res) => { - try { - await transport.handlePostMessage(req, res); - } catch (error) { - console.error('Error handling request:', error); - if (!res.headersSent) res.writeHead(500).end(); - } - }); - - const baseUrl = await listenOnRandomPort(server); - - const addr = server.address(); - const port = typeof addr === 'string' ? new URL(baseUrl).port : (addr as unknown as { port: number }).port; - - return { server, transport, mcpServer, baseUrl, sessionId, serverPort: Number(port) }; - } - - describe('SSEServerTransport', () => { - async function initializeServer(baseUrl: URL): Promise { - const response = await sendSsePostRequest(baseUrl, { - jsonrpc: '2.0', - method: 'initialize', - params: { - clientInfo: { name: 'test-client', version: '1.0' }, - protocolVersion: '2025-03-26', - capabilities: {} - }, - - id: 'init-1' - } as JSONRPCMessage); - - expect(response.status).toBe(202); - - const text = await readAllSSEEvents(response); - - expect(text).toHaveLength(1); - expect(text[0]).toBe('Accepted'); - } - - describe('start method', () => { - it('should correctly append sessionId to a simple relative endpoint', async () => { - const mockRes = createMockResponse(); - const endpoint = '/messages'; - const transport = new SSEServerTransport(endpoint, mockRes); - const expectedSessionId = transport.sessionId; - - await transport.start(); - - expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object)); - expect(mockRes.write).toHaveBeenCalledTimes(1); - expect(mockRes.write).toHaveBeenCalledWith(`event: endpoint\ndata: /messages?sessionId=${expectedSessionId}\n\n`); - }); - - it('should correctly append sessionId to an endpoint with existing query parameters', async () => { - const mockRes = createMockResponse(); - const endpoint = '/messages?foo=bar&baz=qux'; - const transport = new SSEServerTransport(endpoint, mockRes); - const expectedSessionId = transport.sessionId; - - await transport.start(); - - expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object)); - expect(mockRes.write).toHaveBeenCalledTimes(1); - expect(mockRes.write).toHaveBeenCalledWith( - `event: endpoint\ndata: /messages?foo=bar&baz=qux&sessionId=${expectedSessionId}\n\n` - ); - }); - - it('should correctly append sessionId to an endpoint with a hash fragment', async () => { - const mockRes = createMockResponse(); - const endpoint = '/messages#section1'; - const transport = new SSEServerTransport(endpoint, mockRes); - const expectedSessionId = transport.sessionId; - - await transport.start(); - - expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object)); - expect(mockRes.write).toHaveBeenCalledTimes(1); - expect(mockRes.write).toHaveBeenCalledWith(`event: endpoint\ndata: /messages?sessionId=${expectedSessionId}#section1\n\n`); - }); - - it('should correctly append sessionId to an endpoint with query parameters and a hash fragment', async () => { - const mockRes = createMockResponse(); - const endpoint = '/messages?key=value#section2'; - const transport = new SSEServerTransport(endpoint, mockRes); - const expectedSessionId = transport.sessionId; - - await transport.start(); - - expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object)); - expect(mockRes.write).toHaveBeenCalledTimes(1); - expect(mockRes.write).toHaveBeenCalledWith( - `event: endpoint\ndata: /messages?key=value&sessionId=${expectedSessionId}#section2\n\n` - ); - }); - - it('should correctly handle the root path endpoint "/"', async () => { - const mockRes = createMockResponse(); - const endpoint = '/'; - const transport = new SSEServerTransport(endpoint, mockRes); - const expectedSessionId = transport.sessionId; - - await transport.start(); - - expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object)); - expect(mockRes.write).toHaveBeenCalledTimes(1); - expect(mockRes.write).toHaveBeenCalledWith(`event: endpoint\ndata: /?sessionId=${expectedSessionId}\n\n`); - }); - - it('should correctly handle an empty string endpoint ""', async () => { - const mockRes = createMockResponse(); - const endpoint = ''; - const transport = new SSEServerTransport(endpoint, mockRes); - const expectedSessionId = transport.sessionId; - - await transport.start(); - - expect(mockRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object)); - expect(mockRes.write).toHaveBeenCalledTimes(1); - expect(mockRes.write).toHaveBeenCalledWith(`event: endpoint\ndata: /?sessionId=${expectedSessionId}\n\n`); - }); - - /** - * Test: Tool With Request Info - */ - it('should pass request info to tool callback', async () => { - const mockRes = createMockResponse(); - const { mcpServer, baseUrl, sessionId, serverPort } = await createTestServerWithSse({ mockRes }); - await initializeServer(baseUrl); - - mcpServer.tool( - 'test-request-info', - 'A simple test tool with request info', - { name: z.string().describe('Name to greet') }, - async ({ name }, { requestInfo }): Promise => { - return { - content: [ - { type: 'text', text: `Hello, ${name}!` }, - { type: 'text', text: `${JSON.stringify(requestInfo)}` } - ] - }; - } - ); - - const toolCallMessage: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'test-request-info', - arguments: { - name: 'Test User' - } - }, - id: 'call-1' - }; - - const response = await sendSsePostRequest(baseUrl, toolCallMessage, sessionId); - - expect(response.status).toBe(202); - - expect(mockRes.write).toHaveBeenCalledWith(`event: endpoint\ndata: /messages?sessionId=${sessionId}\n\n`); - - const expectedMessage = { - result: { - content: [ - { - type: 'text', - text: 'Hello, Test User!' - }, - { - type: 'text', - text: JSON.stringify({ - headers: { - host: `127.0.0.1:${serverPort}`, - connection: 'keep-alive', - 'content-type': 'application/json', - accept: 'application/json, text/event-stream', - 'accept-language': '*', - 'sec-fetch-mode': 'cors', - 'user-agent': 'node', - 'accept-encoding': 'gzip, deflate', - 'content-length': '124' - } - }) - } - ] - }, - jsonrpc: '2.0', - id: 'call-1' - }; - expect(mockRes.write).toHaveBeenCalledWith(`event: message\ndata: ${JSON.stringify(expectedMessage)}\n\n`); - }); - }); - - describe('handlePostMessage method', () => { - it('should return 500 if server has not started', async () => { - const mockReq = createMockRequest(); - const mockRes = createMockResponse(); - const endpoint = '/messages'; - const transport = new SSEServerTransport(endpoint, mockRes); - - const error = 'SSE connection not established'; - await expect(transport.handlePostMessage(mockReq, mockRes)).rejects.toThrow(error); - expect(mockRes.writeHead).toHaveBeenCalledWith(500); - expect(mockRes.end).toHaveBeenCalledWith(error); - }); - - it('should return 400 if content-type is not application/json', async () => { - const mockReq = createMockRequest({ headers: { 'content-type': 'text/plain' } }); - const mockRes = createMockResponse(); - const endpoint = '/messages'; - const transport = new SSEServerTransport(endpoint, mockRes); - await transport.start(); - - transport.onerror = vi.fn(); - const error = 'Unsupported content-type: text/plain'; - await expect(transport.handlePostMessage(mockReq, mockRes)).resolves.toBe(undefined); - expect(mockRes.writeHead).toHaveBeenCalledWith(400); - expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining(error)); - expect(transport.onerror).toHaveBeenCalledWith(new Error(error)); - }); - - it('should return 400 if message has not a valid schema', async () => { - const invalidMessage = JSON.stringify({ - // missing jsonrpc field - method: 'call', - params: [1, 2, 3], - id: 1 - }); - const mockReq = createMockRequest({ - headers: { 'content-type': 'application/json' }, - body: invalidMessage - }); - const mockRes = createMockResponse(); - const endpoint = '/messages'; - const transport = new SSEServerTransport(endpoint, mockRes); - await transport.start(); - - transport.onmessage = vi.fn(); - await transport.handlePostMessage(mockReq, mockRes); - expect(mockRes.writeHead).toHaveBeenCalledWith(400); - expect(transport.onmessage).not.toHaveBeenCalled(); - expect(mockRes.end).toHaveBeenCalledWith(`Invalid message: ${invalidMessage}`); - }); - - it('should return 202 if message has a valid schema', async () => { - const validMessage = JSON.stringify({ - jsonrpc: '2.0', - method: 'call', - params: { - a: 1, - b: 2, - c: 3 - }, - id: 1 - }); - const mockReq = createMockRequest({ - headers: { 'content-type': 'application/json' }, - body: validMessage - }); - const mockRes = createMockResponse(); - const endpoint = '/messages'; - const transport = new SSEServerTransport(endpoint, mockRes); - await transport.start(); - - transport.onmessage = vi.fn(); - await transport.handlePostMessage(mockReq, mockRes); - expect(mockRes.writeHead).toHaveBeenCalledWith(202); - expect(mockRes.end).toHaveBeenCalledWith('Accepted'); - expect(transport.onmessage).toHaveBeenCalledWith( - { - jsonrpc: '2.0', - method: 'call', - params: { - a: 1, - b: 2, - c: 3 - }, - id: 1 - }, - { - authInfo: { - token: 'test-token' - }, - requestInfo: { - headers: { - 'content-type': 'application/json' - } - } - } - ); - }); - }); - - describe('close method', () => { - it('should call onclose', async () => { - const mockRes = createMockResponse(); - const endpoint = '/messages'; - const transport = new SSEServerTransport(endpoint, mockRes); - await transport.start(); - transport.onclose = vi.fn(); - await transport.close(); - expect(transport.onclose).toHaveBeenCalled(); - }); - }); - - describe('send method', () => { - it('should call onsend', async () => { - const mockRes = createMockResponse(); - const endpoint = '/messages'; - const transport = new SSEServerTransport(endpoint, mockRes); - await transport.start(); - expect(mockRes.write).toHaveBeenCalledTimes(1); - expect(mockRes.write).toHaveBeenCalledWith(expect.stringContaining('event: endpoint')); - expect(mockRes.write).toHaveBeenCalledWith(expect.stringContaining(`data: /messages?sessionId=${transport.sessionId}`)); - }); - }); - - describe('DNS rebinding protection', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('Host header validation', () => { - it('should accept requests with allowed host headers', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes, { - allowedHosts: ['localhost:3000', 'example.com'], - enableDnsRebindingProtection: true - }); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - host: 'localhost:3000', - 'content-type': 'application/json' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202); - expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); - }); - - it('should reject requests with disallowed host headers', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes, { - allowedHosts: ['localhost:3000'], - enableDnsRebindingProtection: true - }); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - host: 'evil.com', - 'content-type': 'application/json' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(403); - expect(mockHandleRes.end).toHaveBeenCalledWith('Invalid Host header: evil.com'); - }); - - it('should reject requests without host header when allowedHosts is configured', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes, { - allowedHosts: ['localhost:3000'], - enableDnsRebindingProtection: true - }); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - 'content-type': 'application/json' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(403); - expect(mockHandleRes.end).toHaveBeenCalledWith('Invalid Host header: undefined'); - }); - }); - - describe('Origin header validation', () => { - it('should accept requests with allowed origin headers', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes, { - allowedOrigins: ['http://localhost:3000', 'https://example.com'], - enableDnsRebindingProtection: true - }); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - origin: 'http://localhost:3000', - 'content-type': 'application/json' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202); - expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); - }); - - it('should accept requests without origin headers', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes, { - allowedOrigins: ['http://localhost:3000', 'https://example.com'], - enableDnsRebindingProtection: true - }); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - 'content-type': 'application/json' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202); - expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); - }); - - it('should reject requests with disallowed origin headers', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes, { - allowedOrigins: ['http://localhost:3000'], - enableDnsRebindingProtection: true - }); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - origin: 'http://evil.com', - 'content-type': 'application/json' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(403); - expect(mockHandleRes.end).toHaveBeenCalledWith('Invalid Origin header: http://evil.com'); - }); - }); - - describe('Content-Type validation', () => { - it('should accept requests with application/json content-type', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - 'content-type': 'application/json' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202); - expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); - }); - - it('should accept requests with application/json with charset', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - 'content-type': 'application/json; charset=utf-8' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202); - expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); - }); - - it('should reject requests with non-application/json content-type when protection is enabled', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - 'content-type': 'text/plain' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(400); - expect(mockHandleRes.end).toHaveBeenCalledWith('Error: Unsupported content-type: text/plain'); - }); - }); - - describe('enableDnsRebindingProtection option', () => { - it('should skip all validations when enableDnsRebindingProtection is false', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes, { - allowedHosts: ['localhost:3000'], - allowedOrigins: ['http://localhost:3000'], - enableDnsRebindingProtection: false - }); - await transport.start(); - - const mockReq = createMockRequest({ - headers: { - host: 'evil.com', - origin: 'http://evil.com', - 'content-type': 'text/plain' - } - }); - const mockHandleRes = createMockResponse(); - - await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); - - // Should pass even with invalid headers because protection is disabled - expect(mockHandleRes.writeHead).toHaveBeenCalledWith(400); - // The error should be from content-type parsing, not DNS rebinding protection - expect(mockHandleRes.end).toHaveBeenCalledWith('Error: Unsupported content-type: text/plain'); - }); - }); - - describe('Combined validations', () => { - it('should validate both host and origin when both are configured', async () => { - const mockRes = createMockResponse(); - const transport = new SSEServerTransport('/messages', mockRes, { - allowedHosts: ['localhost:3000'], - allowedOrigins: ['http://localhost:3000'], - enableDnsRebindingProtection: true - }); - await transport.start(); - - // Valid host, invalid origin - const mockReq1 = createMockRequest({ - headers: { - host: 'localhost:3000', - origin: 'http://evil.com', - 'content-type': 'application/json' - } - }); - const mockHandleRes1 = createMockResponse(); - - await transport.handlePostMessage(mockReq1, mockHandleRes1, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes1.writeHead).toHaveBeenCalledWith(403); - expect(mockHandleRes1.end).toHaveBeenCalledWith('Invalid Origin header: http://evil.com'); - - // Invalid host, valid origin - const mockReq2 = createMockRequest({ - headers: { - host: 'evil.com', - origin: 'http://localhost:3000', - 'content-type': 'application/json' - } - }); - const mockHandleRes2 = createMockResponse(); - - await transport.handlePostMessage(mockReq2, mockHandleRes2, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes2.writeHead).toHaveBeenCalledWith(403); - expect(mockHandleRes2.end).toHaveBeenCalledWith('Invalid Host header: evil.com'); - - // Both valid - const mockReq3 = createMockRequest({ - headers: { - host: 'localhost:3000', - origin: 'http://localhost:3000', - 'content-type': 'application/json' - } - }); - const mockHandleRes3 = createMockResponse(); - - await transport.handlePostMessage(mockReq3, mockHandleRes3, { jsonrpc: '2.0', method: 'test' }); - - expect(mockHandleRes3.writeHead).toHaveBeenCalledWith(202); - expect(mockHandleRes3.end).toHaveBeenCalledWith('Accepted'); - }); - }); - }); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2b9d0835..6c249da48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,7 +236,7 @@ importers: version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + version: 2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) eslint-plugin-n: specifier: catalog:devTools version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) @@ -314,6 +314,9 @@ importers: '@modelcontextprotocol/server-hono': specifier: workspace:^ version: link:../../packages/server-hono + better-auth: + specifier: ^1.4.7 + version: 1.4.7(better-sqlite3@11.10.0)(vitest@4.0.9(@types/node@24.10.3)(tsx@4.20.6)) cors: specifier: catalog:runtimeServerOnly version: 2.8.5 @@ -348,12 +351,21 @@ importers: examples/shared: dependencies: + '@modelcontextprotocol/core': + specifier: workspace:^ + version: link:../../packages/core '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server '@modelcontextprotocol/server-express': specifier: workspace:^ version: link:../../packages/server-express + better-auth: + specifier: ^1.4.7 + version: 1.4.7(better-sqlite3@11.10.0)(vitest@4.0.9(@types/node@24.10.3)(tsx@4.20.6)) + better-sqlite3: + specifier: ^11.10.0 + version: 11.10.0 express: specifier: catalog:runtimeServerOnly version: 5.1.0 @@ -373,6 +385,9 @@ importers: '@modelcontextprotocol/vitest-config': specifier: workspace:^ version: link:../../common/vitest-config + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 '@types/express': specifier: catalog:devTools version: 5.0.5 @@ -868,6 +883,27 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@better-auth/core@1.4.7': + resolution: {integrity: sha512-rNfj8aNFwPwAMYo+ahoWDsqKrV7svD3jhHSC6+A77xxKodbgV0UgH+RO21GMaZ0PPAibEl851nw5e3bsNslW/w==} + peerDependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + better-call: 1.1.5 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + + '@better-auth/telemetry@1.4.7': + resolution: {integrity: sha512-k07C/FWnX6m+IxLruNkCweIxuaIwVTB2X40EqwamRVhYNBAhOYZFGLHH+PtQyM+Yf1Z4+8H6MugLOXSreXNAjQ==} + peerDependencies: + '@better-auth/core': 1.4.7 + + '@better-auth/utils@0.3.0': + resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} + + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} @@ -1191,10 +1227,18 @@ packages: '@napi-rs/wasm-runtime@1.1.0': resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1418,6 +1462,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -1824,13 +1871,92 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-auth@1.4.7: + resolution: {integrity: sha512-kVmDQxzqGwP4FFMOYpS5I7oAaoFW3hwooUAAtcbb2DrOYv5EUvRUDJbTMaPoMTj7URjNDQ6vG9gcCS1Q+0aVBw==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.22.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + better-sqlite3: ^12.4.1 + drizzle-kit: ^0.31.4 + drizzle-orm: ^0.41.0 + mongodb: ^6.18.0 + mysql2: ^3.14.4 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.16.3 + prisma: ^5.22.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^4.0.15 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.1.5: + resolution: {integrity: sha512-nQJ3S87v6wApbDwbZ++FrQiSiVxWvZdjaO+2v6lZJAG2WWggkB2CziUDjPciz3eAt9TqfRursIQMZIcpkBnvlw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + birpc@3.0.0: resolution: {integrity: sha512-by+04pHuxpCEQcucAXqzopqfhyI8TLK5Qg5MST0cB6MP+JhHna9ollrtK9moVh27aq6Q6MEJgebD0cVm//yBkg==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} @@ -1845,6 +1971,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1880,6 +2009,9 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -1960,6 +2092,14 @@ packages: supports-color: optional: true + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1986,6 +2126,10 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -2025,6 +2169,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -2223,6 +2370,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -2275,6 +2426,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2318,6 +2472,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2360,6 +2517,9 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2444,6 +2604,9 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2467,6 +2630,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -2640,6 +2806,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kysely@0.28.9: + resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==} + engines: {node: '>=20.0.0'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2706,6 +2876,10 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2716,6 +2890,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2728,6 +2905,13 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@1.1.0: + resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} + engines: {node: ^20.0.0 || >=22.0.0} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2740,6 +2924,10 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-abi@3.85.0: + resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} + engines: {node: '>=10'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2885,6 +3073,11 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2903,6 +3096,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2928,10 +3124,18 @@ packages: resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} engines: {node: '>= 0.10'} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2993,6 +3197,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -3035,6 +3242,9 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3081,6 +3291,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3129,6 +3345,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3137,6 +3356,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -3161,6 +3384,13 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -3255,6 +3485,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3325,6 +3558,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3474,6 +3710,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + snapshots: '@babel/generator@7.28.5': @@ -3499,6 +3738,27 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)': + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.0.0 + better-call: 1.1.5(zod@4.2.1) + jose: 6.1.3 + kysely: 0.28.9 + nanostores: 1.1.0 + zod: 4.2.1 + + '@better-auth/telemetry@1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))': + dependencies: + '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.3.0': {} + + '@better-fetch/fetch@1.1.21': {} + '@cfworker/json-schema@4.1.1': {} '@changesets/apply-release-plan@7.0.14': @@ -3866,8 +4126,12 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@noble/ciphers@2.1.1': {} + '@noble/hashes@1.8.0': {} + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4010,6 +4274,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 24.10.3 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -4442,12 +4710,56 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + + better-auth@1.4.7(better-sqlite3@11.10.0)(vitest@4.0.9(@types/node@24.10.3)(tsx@4.20.6)): + dependencies: + '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.1.5(zod@4.2.1) + defu: 6.1.4 + jose: 6.1.3 + kysely: 0.28.9 + nanostores: 1.1.0 + zod: 4.2.1 + optionalDependencies: + better-sqlite3: 11.10.0 + vitest: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + + better-call@1.1.5(zod@4.2.1): + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 2.7.2 + optionalDependencies: + zod: 4.2.1 + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + better-sqlite3@11.10.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + birpc@3.0.0: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + body-parser@2.2.0: dependencies: bytes: 3.1.2 @@ -4475,6 +4787,11 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bytes@3.1.2: {} cac@6.7.14: {} @@ -4507,6 +4824,8 @@ snapshots: chardet@2.1.1: {} + chownr@1.1.4: {} + ci-info@3.9.0: {} color-convert@2.0.1: @@ -4574,6 +4893,12 @@ snapshots: dependencies: ms: 2.1.3 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -4596,6 +4921,8 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@2.1.2: {} + dezalgo@1.0.4: dependencies: asap: 2.0.6 @@ -4625,6 +4952,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -4787,15 +5118,14 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) eslint: 9.39.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1) @@ -4809,7 +5139,7 @@ snapshots: eslint: 9.39.1 eslint-compat-utils: 0.5.1(eslint@9.39.1) - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -4820,7 +5150,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -4831,8 +5161,6 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -4937,6 +5265,8 @@ snapshots: dependencies: eventsource-parser: 3.0.6 + expand-template@2.0.3: {} + expect-type@1.2.2: {} express-rate-limit@8.2.1(express@5.1.0): @@ -5008,6 +5338,8 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -5062,6 +5394,8 @@ snapshots: fresh@2.0.0: {} + fs-constants@1.0.0: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -5120,6 +5454,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -5196,6 +5532,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -5211,6 +5549,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -5380,6 +5720,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kysely@0.28.9: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -5430,6 +5772,8 @@ snapshots: mime@2.6.0: {} + mimic-response@3.1.0: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -5440,18 +5784,28 @@ snapshots: minimist@1.2.8: {} + mkdirp-classic@0.5.3: {} + mri@1.2.0: {} ms@2.1.3: {} nanoid@3.3.11: {} + nanostores@1.1.0: {} + + napi-build-utils@2.0.0: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} negotiator@1.0.0: {} + node-abi@3.85.0: + dependencies: + semver: 7.7.3 + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -5582,6 +5936,21 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.85.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + prelude-ls@1.2.1: {} prettier@2.8.8: {} @@ -5593,6 +5962,11 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} qs@6.14.0: @@ -5614,6 +5988,13 @@ snapshots: iconv-lite: 0.7.0 unpipe: 1.0.0 + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -5621,6 +6002,12 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -5722,6 +6109,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.2 fsevents: 2.3.3 + rou3@0.7.12: {} + router@2.2.0: dependencies: debug: 4.4.3 @@ -5788,6 +6177,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -5850,6 +6241,14 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + slash@3.0.0: {} source-map-js@1.2.1: {} @@ -5899,12 +6298,18 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 strip-bom@3.0.0: {} + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} superagent@10.2.3: @@ -5936,6 +6341,21 @@ snapshots: tapable@2.3.0: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + term-size@2.2.1: {} tinybench@2.9.0: {} @@ -6017,6 +6437,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6123,6 +6547,8 @@ snapshots: dependencies: punycode: 2.3.1 + util-deprecate@1.0.2: {} + vary@1.1.2: {} vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.3)(tsx@4.20.6)): @@ -6257,3 +6683,5 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} + + zod@4.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a7222dd71..0d86e1799 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,56 +1,60 @@ packages: - - packages/**/* - - common/**/* - - examples/**/* - - test/**/* + - packages/**/* + - common/**/* + - examples/**/* + - test/**/* catalogs: - runtimeShared: - ajv: ^8.17.1 - ajv-formats: ^3.0.1 - json-schema-typed: ^8.0.2 - pkce-challenge: ^5.0.0 - zod: ^3.25 || ^4.0 - zod-to-json-schema: ^3.25.0 - '@cfworker/json-schema': ^4.1.1 - runtimeServerOnly: - '@hono/node-server': ^1.19.7 - hono: ^4.11.1 - content-type: ^1.0.5 - cors: ^2.8.5 - express: ^5.0.1 - express-rate-limit: ^8.2.1 - raw-body: ^3.0.0 - '@remix-run/node-fetch-server': ^0.13.0 - runtimeClientOnly: - jose: ^6.1.1 - cross-spawn: ^7.0.5 - eventsource: ^3.0.2 - eventsource-parser: ^3.0.0 - devTools: - typescript: ^5.9.3 - vitest: ^4.0.8 - eslint: ^9.8.0 - '@eslint/js': ^9.39.1 - eslint-config-prettier: ^10.1.8 - eslint-plugin-n: ^17.23.1 - prettier: 3.6.2 - tsx: ^4.16.5 - 'typescript-eslint': ^8.48.1 - supertest: ^7.0.0 - ws: ^8.18.0 - vite-tsconfig-paths: ^5.1.4 - '@types/content-type': ^1.1.8 - '@types/cors': ^2.8.17 - '@types/cross-spawn': ^6.0.6 - '@types/eventsource': ^1.1.15 - '@types/express': ^5.0.0 - '@types/express-serve-static-core': ^5.1.0 - '@types/supertest': ^6.0.2 - '@types/ws': ^8.5.12 - '@typescript/native-preview': ^7.0.0-dev.20251217.1 - tsdown: ^0.18.0 + devTools: + '@eslint/js': ^9.39.1 + '@types/content-type': ^1.1.8 + '@types/cors': ^2.8.17 + '@types/cross-spawn': ^6.0.6 + '@types/eventsource': ^1.1.15 + '@types/express': ^5.0.0 + '@types/express-serve-static-core': ^5.1.0 + '@types/supertest': ^6.0.2 + '@types/ws': ^8.5.12 + '@typescript/native-preview': ^7.0.0-dev.20251217.1 + eslint: ^9.8.0 + eslint-config-prettier: ^10.1.8 + eslint-plugin-n: ^17.23.1 + prettier: 3.6.2 + supertest: ^7.0.0 + tsdown: ^0.18.0 + tsx: ^4.16.5 + typescript: ^5.9.3 + typescript-eslint: ^8.48.1 + vite-tsconfig-paths: ^5.1.4 + vitest: ^4.0.8 + ws: ^8.18.0 + runtimeClientOnly: + cross-spawn: ^7.0.5 + eventsource: ^3.0.2 + eventsource-parser: ^3.0.0 + jose: ^6.1.1 + runtimeServerOnly: + '@hono/node-server': ^1.19.7 + '@remix-run/node-fetch-server': ^0.13.0 + content-type: ^1.0.5 + cors: ^2.8.5 + express: ^5.0.1 + express-rate-limit: ^8.2.1 + hono: ^4.11.1 + raw-body: ^3.0.0 + runtimeShared: + '@cfworker/json-schema': ^4.1.1 + ajv: ^8.17.1 + ajv-formats: ^3.0.1 + json-schema-typed: ^8.0.2 + pkce-challenge: ^5.0.0 + zod: ^3.25 || ^4.0 + zod-to-json-schema: ^3.25.0 enableGlobalVirtualStore: false linkWorkspacePackages: deep + +onlyBuiltDependencies: + - better-sqlite3 + - esbuild From d52565ea3dbca74dc0a114bc39bbc6df0a17dbbc Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 21 Dec 2025 17:14:45 +0200 Subject: [PATCH 07/23] lint fix --- examples/shared/test/demoInMemoryOAuthProvider.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/shared/test/demoInMemoryOAuthProvider.test.ts b/examples/shared/test/demoInMemoryOAuthProvider.test.ts index 5ea1fea9c..1c798047f 100644 --- a/examples/shared/test/demoInMemoryOAuthProvider.test.ts +++ b/examples/shared/test/demoInMemoryOAuthProvider.test.ts @@ -9,8 +9,8 @@ import { describe, expect, it } from 'vitest'; -import type {CreateDemoAuthOptions} from '../src/auth.js'; -import { createDemoAuth } from '../src/auth.js'; +import type { CreateDemoAuthOptions } from '../src/auth.js'; +import { createDemoAuth } from '../src/auth.js'; describe('createDemoAuth', () => { const validOptions: CreateDemoAuthOptions = { From 62656b462e8612dd05e1658fac390e42097ac5ea Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 21 Dec 2025 19:07:48 +0200 Subject: [PATCH 08/23] oAuth metadata: switch to better-auth/plugins -> mcp plugin --- examples/server/src/elicitationUrlExample.ts | 18 +++----- examples/server/src/simpleStreamableHttp.ts | 17 +++----- examples/shared/src/auth.ts | 3 +- examples/shared/src/authMiddleware.ts | 43 +------------------- examples/shared/src/authServer.ts | 39 +++++++++++------- examples/shared/src/index.ts | 6 +-- 6 files changed, 42 insertions(+), 84 deletions(-) diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index b3d433214..39a591708 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -10,12 +10,12 @@ import { randomUUID } from 'node:crypto'; import { + createProtectedResourceMetadataRouter, getOAuthProtectedResourceMetadataUrl, - mcpAuthMetadataRouter, requireBearerAuth, setupAuthServer } from '@modelcontextprotocol/examples-shared'; -import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server'; +import type { CallToolResult, ElicitRequestURLParams, ElicitResult } from '@modelcontextprotocol/server'; import { isInitializeRequest, McpServer, @@ -239,17 +239,11 @@ let authMiddleware = null; const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`); const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); -const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true }); +setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true }); -// Add metadata routes to the main MCP server -app.use( - mcpAuthMetadataRouter({ - oauthMetadata, - resourceServerUrl: mcpServerUrl, - scopesSupported: ['mcp:tools'], - resourceName: 'MCP Demo Server' - }) -); +// Add protected resource metadata route to the MCP server +// This allows clients to discover the auth server +app.use(createProtectedResourceMetadataRouter()); authMiddleware = requireBearerAuth({ requiredScopes: [], diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index b90820266..b62b992f1 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -1,15 +1,14 @@ import { randomUUID } from 'node:crypto'; import { + createProtectedResourceMetadataRouter, getOAuthProtectedResourceMetadataUrl, - mcpAuthMetadataRouter, requireBearerAuth, setupAuthServer } from '@modelcontextprotocol/examples-shared'; import type { CallToolResult, GetPromptResult, - OAuthMetadata, PrimitiveSchemaDefinition, ReadResourceResult, ResourceLink @@ -528,17 +527,11 @@ if (useOAuth) { const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`); const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); - const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth }); + setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth }); - // Add metadata routes to the main MCP server - app.use( - mcpAuthMetadataRouter({ - oauthMetadata, - resourceServerUrl: mcpServerUrl, - scopesSupported: ['mcp:tools'], - resourceName: 'MCP Demo Server' - }) - ); + // Add protected resource metadata route to the MCP server + // This allows clients to discover the auth server + app.use(createProtectedResourceMetadataRouter()); authMiddleware = requireBearerAuth({ requiredScopes: [], diff --git a/examples/shared/src/auth.ts b/examples/shared/src/auth.ts index 8bf9c8c11..4533be355 100644 --- a/examples/shared/src/auth.ts +++ b/examples/shared/src/auth.ts @@ -7,7 +7,6 @@ * For production use, configure a proper database and authentication flow. */ -import type { BetterAuthPlugin } from 'better-auth'; import { betterAuth } from 'better-auth'; import { mcp } from 'better-auth/plugins'; import Database from 'better-sqlite3'; @@ -70,7 +69,7 @@ export function createDemoAuth(options: CreateDemoAuthOptions) { enabled: true, requireEmailVerification: false }, - plugins: [mcpPlugin as BetterAuthPlugin] + plugins: [mcpPlugin] }); } diff --git a/examples/shared/src/authMiddleware.ts b/examples/shared/src/authMiddleware.ts index e46d9e410..ff298fdfe 100644 --- a/examples/shared/src/authMiddleware.ts +++ b/examples/shared/src/authMiddleware.ts @@ -3,12 +3,10 @@ * * 🚨 DEMO ONLY - NOT FOR PRODUCTION * - * This provides bearer auth middleware and metadata routes for MCP servers. + * This provides bearer auth middleware for MCP servers. */ -import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/server'; -import type { NextFunction, Request, Response, Router } from 'express'; -import express from 'express'; +import type { NextFunction, Request, Response } from 'express'; import { verifyAccessToken } from './authServer.js'; @@ -78,43 +76,6 @@ export function requireBearerAuth( }; } -export interface McpAuthMetadataRouterOptions { - oauthMetadata: OAuthMetadata; - resourceServerUrl: URL; - scopesSupported?: string[]; - resourceName?: string; -} - -/** - * Creates an Express router that serves OAuth and Protected Resource metadata. - */ -export function mcpAuthMetadataRouter(options: McpAuthMetadataRouterOptions): Router { - const { oauthMetadata, resourceServerUrl, scopesSupported = ['mcp:tools'], resourceName } = options; - - const router = express.Router(); - - // OAuth Protected Resource Metadata (RFC 9728) - const protectedResourceMetadata: OAuthProtectedResourceMetadata = { - resource: resourceServerUrl.toString(), - authorization_servers: [oauthMetadata.issuer], - scopes_supported: scopesSupported, - resource_name: resourceName - }; - - // Serve protected resource metadata - router.get('/.well-known/oauth-protected-resource', (req: Request, res: Response) => { - res.json(protectedResourceMetadata); - }); - - // Also serve at the MCP-specific path - const mcpPath = new URL(resourceServerUrl.pathname, resourceServerUrl).pathname; - router.get(`${mcpPath}/.well-known/oauth-protected-resource`, (req: Request, res: Response) => { - res.json(protectedResourceMetadata); - }); - - return router; -} - /** * Helper to get the protected resource metadata URL from a server URL. */ diff --git a/examples/shared/src/authServer.ts b/examples/shared/src/authServer.ts index 9a14e8978..0fb906d6f 100644 --- a/examples/shared/src/authServer.ts +++ b/examples/shared/src/authServer.ts @@ -11,7 +11,8 @@ import type { OAuthMetadata } from '@modelcontextprotocol/core'; import { toNodeHandler } from 'better-auth/node'; -import type { Request, Response as ExpressResponse } from 'express'; +import { oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata } from 'better-auth/plugins'; +import type { Request, Response as ExpressResponse, Router } from 'express'; import express from 'express'; import type { DemoAuth } from './auth.js'; @@ -132,19 +133,10 @@ export function setupAuthServer(options: SetupAuthServerOptions): OAuthMetadata // This handles: authorization, token, client registration, etc. authApp.all('/api/auth/*', toNodeHandler(auth)); - // OAuth metadata endpoints at well-known paths - // Some clients may not parse WWW-Authenticate header and need these - authApp.get('/.well-known/oauth-authorization-server', (_req, res) => { - res.json(createOAuthMetadata(authServerUrl)); - }); - - authApp.get('/.well-known/oauth-protected-resource', (_req, res) => { - res.json({ - resource: mcpServerUrl.toString(), - authorization_servers: [authServerUrl.toString().replace(/\/$/, '')], - scopes_supported: ['openid', 'profile', 'email', 'mcp:tools'] - }); - }); + // OAuth metadata endpoints using better-auth's built-in handlers + // See: https://www.better-auth.com/docs/plugins/mcp#oauth-discovery-metadata + authApp.get('/.well-known/oauth-authorization-server', toNodeHandler(oAuthDiscoveryMetadata(auth))); + authApp.get('/.well-known/oauth-protected-resource', toNodeHandler(oAuthProtectedResourceMetadata(auth))); // Start the auth server const authPort = parseInt(authServerUrl.port, 10); @@ -162,6 +154,25 @@ export function setupAuthServer(options: SetupAuthServerOptions): OAuthMetadata return createOAuthMetadata(authServerUrl); } +/** + * Creates an Express router that serves OAuth Protected Resource Metadata + * on the MCP server using better-auth's built-in handler. + * + * This is needed because MCP clients discover the auth server by first + * fetching protected resource metadata from the MCP server. + * + * See: https://www.better-auth.com/docs/plugins/mcp#oauth-protected-resource-metadata + */ +export function createProtectedResourceMetadataRouter(): Router { + const auth = getAuth(); + const router = express.Router(); + + // Serve at the standard well-known path + router.get('/.well-known/oauth-protected-resource', toNodeHandler(oAuthProtectedResourceMetadata(auth))); + + return router; +} + /** * Creates OAuth 2.0 Authorization Server Metadata (RFC 8414) */ diff --git a/examples/shared/src/index.ts b/examples/shared/src/index.ts index 21d189bbc..e353700bc 100644 --- a/examples/shared/src/index.ts +++ b/examples/shared/src/index.ts @@ -3,9 +3,9 @@ export type { CreateDemoAuthOptions, DemoAuth } from './auth.js'; export { createDemoAuth } from './auth.js'; // Auth middleware -export type { McpAuthMetadataRouterOptions, RequireBearerAuthOptions } from './authMiddleware.js'; -export { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, requireBearerAuth } from './authMiddleware.js'; +export type { RequireBearerAuthOptions } from './authMiddleware.js'; +export { getOAuthProtectedResourceMetadataUrl, requireBearerAuth } from './authMiddleware.js'; // Auth server setup export type { AuthServerResult, SetupAuthServerOptions } from './authServer.js'; -export { getAuth, setupAuthServer, verifyAccessToken } from './authServer.js'; +export { createProtectedResourceMetadataRouter, getAuth, setupAuthServer, verifyAccessToken } from './authServer.js'; From 7dc044ba0eaa87918eb44e4abc5271fcdd16286f Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 22 Dec 2025 00:50:40 +0200 Subject: [PATCH 09/23] working better-auth example --- examples/shared/src/auth.ts | 166 ++++++++++++++++++++++++- examples/shared/src/authServer.ts | 196 +++++++++++++++++++----------- 2 files changed, 287 insertions(+), 75 deletions(-) diff --git a/examples/shared/src/auth.ts b/examples/shared/src/auth.ts index 4533be355..e2c6aca54 100644 --- a/examples/shared/src/auth.ts +++ b/examples/shared/src/auth.ts @@ -7,10 +7,15 @@ * For production use, configure a proper database and authentication flow. */ +import { randomBytes } from 'node:crypto'; + import { betterAuth } from 'better-auth'; import { mcp } from 'better-auth/plugins'; import Database from 'better-sqlite3'; +// Generate a random password for the demo user (new each time the server starts) +const DEMO_PASSWORD = randomBytes(16).toString('base64url'); + // Create the in-memory database once (module-level singleton) // This avoids the type export issue and ensures the same DB is used let _db: InstanceType | null = null; @@ -18,10 +23,152 @@ let _db: InstanceType | null = null; function getDatabase(): InstanceType { if (!_db) { _db = new Database(':memory:'); + initializeSchema(_db); } return _db; } +/** + * Initialize the database schema for better-auth + MCP plugin. + * This creates all required tables for the demo. + * + * Schema based on: + * - https://www.better-auth.com/docs/concepts/database#core-schema + * - https://www.better-auth.com/docs/plugins/oidc-provider#schema + */ +function initializeSchema(db: InstanceType): void { + // Core better-auth tables + db.exec(` + CREATE TABLE IF NOT EXISTS user ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + emailVerified INTEGER NOT NULL DEFAULT 0, + image TEXT, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS session ( + id TEXT PRIMARY KEY, + token TEXT NOT NULL UNIQUE, + expiresAt TEXT NOT NULL, + ipAddress TEXT, + userAgent TEXT, + userId TEXT NOT NULL REFERENCES user(id), + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS account ( + id TEXT PRIMARY KEY, + accountId TEXT NOT NULL, + providerId TEXT NOT NULL, + userId TEXT NOT NULL REFERENCES user(id), + accessToken TEXT, + refreshToken TEXT, + idToken TEXT, + accessTokenExpiresAt TEXT, + refreshTokenExpiresAt TEXT, + scope TEXT, + password TEXT, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS verification ( + id TEXT PRIMARY KEY, + identifier TEXT NOT NULL, + value TEXT NOT NULL, + expiresAt TEXT NOT NULL, + createdAt TEXT, + updatedAt TEXT + ); + `); + + // OIDC/MCP plugin tables + db.exec(` + CREATE TABLE IF NOT EXISTS oauthApplication ( + id TEXT PRIMARY KEY, + name TEXT, + icon TEXT, + metadata TEXT, + clientId TEXT NOT NULL UNIQUE, + clientSecret TEXT, + redirectUrls TEXT NOT NULL, + type TEXT NOT NULL, + disabled INTEGER NOT NULL DEFAULT 0, + userId TEXT, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS oauthAccessToken ( + id TEXT PRIMARY KEY, + accessToken TEXT NOT NULL UNIQUE, + refreshToken TEXT UNIQUE, + accessTokenExpiresAt TEXT NOT NULL, + refreshTokenExpiresAt TEXT, + clientId TEXT NOT NULL, + userId TEXT, + scopes TEXT NOT NULL, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS oauthRefreshToken ( + id TEXT PRIMARY KEY, + refreshToken TEXT NOT NULL UNIQUE, + accessTokenId TEXT NOT NULL, + expiresAt TEXT NOT NULL, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS oauthAuthorizationCode ( + id TEXT PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + clientId TEXT NOT NULL, + userId TEXT, + scopes TEXT NOT NULL, + redirectURI TEXT NOT NULL, + codeChallenge TEXT, + codeChallengeMethod TEXT, + expiresAt TEXT NOT NULL, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS oauthConsent ( + id TEXT PRIMARY KEY, + clientId TEXT NOT NULL, + userId TEXT NOT NULL, + scopes TEXT NOT NULL, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL, + consentGiven INTEGER NOT NULL DEFAULT 0 + ); + `); + + console.log('[Auth] In-memory database schema initialized'); + console.log('[Auth] ========================================'); + console.log('[Auth] Demo user credentials (auto-login):'); + console.log(`[Auth] Email: ${DEMO_USER_CREDENTIALS.email}`); + console.log(`[Auth] Password: ${DEMO_USER_CREDENTIALS.password}`); + console.log('[Auth] ========================================'); +} + +/** + * Demo user credentials for auto-login. + * Password is randomly generated each time the server starts. + * Used by authServer.ts to create and sign in the demo user. + */ +export const DEMO_USER_CREDENTIALS = { + email: 'demo@example.com', + password: DEMO_PASSWORD, + name: 'Demo User' +}; + export interface CreateDemoAuthOptions { baseURL: string; resource?: string; @@ -55,7 +202,8 @@ export function createDemoAuth(options: CreateDemoAuthOptions) { accessTokenExpiresIn: 3600, // 1 hour refreshTokenExpiresIn: 604800, // 7 days defaultScope: 'openid', - scopes: ['openid', 'profile', 'email', 'offline_access', 'mcp:tools'] + scopes: ['openid', 'profile', 'email', 'offline_access', 'mcp:tools'], + allowDynamicClientRegistration: true } }); @@ -69,7 +217,21 @@ export function createDemoAuth(options: CreateDemoAuthOptions) { enabled: true, requireEmailVerification: false }, - plugins: [mcpPlugin] + plugins: [mcpPlugin], + // Enable verbose logging for demo/debugging + logger: { + disabled: false, + level: 'debug', + log: (level, message, ...args) => { + const timestamp = new Date().toISOString(); + const prefix = `[Auth ${level.toUpperCase()}]`; + if (args.length > 0) { + console.log(`${timestamp} ${prefix} ${message}`, ...args); + } else { + console.log(`${timestamp} ${prefix} ${message}`); + } + } + } }); } diff --git a/examples/shared/src/authServer.ts b/examples/shared/src/authServer.ts index 0fb906d6f..7147f83e8 100644 --- a/examples/shared/src/authServer.ts +++ b/examples/shared/src/authServer.ts @@ -9,14 +9,13 @@ * See: https://www.better-auth.com/docs/plugins/mcp */ -import type { OAuthMetadata } from '@modelcontextprotocol/core'; import { toNodeHandler } from 'better-auth/node'; import { oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata } from 'better-auth/plugins'; import type { Request, Response as ExpressResponse, Router } from 'express'; import express from 'express'; import type { DemoAuth } from './auth.js'; -import { createDemoAuth } from './auth.js'; +import { createDemoAuth, DEMO_USER_CREDENTIALS } from './auth.js'; export interface SetupAuthServerOptions { authServerUrl: URL; @@ -24,13 +23,9 @@ export interface SetupAuthServerOptions { strictResource?: boolean; } -export interface AuthServerResult { - auth: DemoAuth; - oauthMetadata: OAuthMetadata; -} - // Store auth instance globally so it can be used for token verification let globalAuth: DemoAuth | null = null; +let demoUserCreated = false; /** * Gets the global auth instance (must call setupAuthServer first) @@ -42,13 +37,44 @@ export function getAuth(): DemoAuth { return globalAuth; } +/** + * Ensures the demo user exists by calling signUpEmail (creates user with proper password hash) + * Returns true if successful, false if user already exists (which is fine) + */ +async function ensureDemoUserExists(auth: DemoAuth): Promise { + if (demoUserCreated) return; + + try { + // Try to sign up the demo user + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (auth.api as any).signUpEmail({ + body: { + email: DEMO_USER_CREDENTIALS.email, + password: DEMO_USER_CREDENTIALS.password, + name: DEMO_USER_CREDENTIALS.name + } + }); + console.log('[Auth] Demo user created via signUpEmail'); + demoUserCreated = true; + } catch (error) { + // User might already exist, which is fine + const message = error instanceof Error ? error.message : String(error); + if (message.includes('already') || message.includes('exists') || message.includes('unique')) { + console.log('[Auth] Demo user already exists'); + demoUserCreated = true; + } else { + console.error('[Auth] Failed to create demo user:', error); + throw error; + } + } +} + /** * Sets up and starts the OAuth Authorization Server on a separate port. * * @param options - Server configuration - * @returns OAuth metadata for the authorization server */ -export function setupAuthServer(options: SetupAuthServerOptions): OAuthMetadata { +export function setupAuthServer(options: SetupAuthServerOptions): void { const { authServerUrl, mcpServerUrl } = options; // Create better-auth instance with MCP plugin @@ -63,10 +89,8 @@ export function setupAuthServer(options: SetupAuthServerOptions): OAuthMetadata // Create Express app for auth server const authApp = express(); - authApp.use(express.json()); - authApp.use(express.urlencoded({ extended: true })); - // Enable CORS for all origins (demo only) + // Enable CORS for all origins (demo only) - must be before other middleware authApp.use((_req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); @@ -79,7 +103,47 @@ export function setupAuthServer(options: SetupAuthServerOptions): OAuthMetadata next(); }); - // Auto-login page that immediately creates a session and redirects + // Request logging middleware for OAuth endpoints + authApp.use('/api/auth', (req, res, next) => { + const timestamp = new Date().toISOString(); + console.log(`${timestamp} [Auth Request] ${req.method} ${req.url}`); + if (req.method === 'POST') { + console.log(`${timestamp} [Auth Request] Content-Type: ${req.headers['content-type']}`); + } + + // Log response when it finishes + const originalSend = res.send.bind(res); + res.send = function(body) { + console.log(`${timestamp} [Auth Response] ${res.statusCode} ${req.url}`); + if (res.statusCode >= 400 && body) { + try { + const parsed = typeof body === 'string' ? JSON.parse(body) : body; + console.log(`${timestamp} [Auth Response] Error:`, parsed); + } catch { + // Not JSON, log as-is if short + if (typeof body === 'string' && body.length < 200) { + console.log(`${timestamp} [Auth Response] Body: ${body}`); + } + } + } + return originalSend(body); + }; + next(); + }); + + // Mount better-auth handler BEFORE body parsers + // toNodeHandler reads the raw request body, so Express must not consume it first + authApp.all('/api/auth/{*splat}', toNodeHandler(auth)); + + // OAuth metadata endpoints using better-auth's built-in handlers + authApp.get('/.well-known/oauth-authorization-server', toNodeHandler(oAuthDiscoveryMetadata(auth))); + authApp.get('/.well-known/oauth-protected-resource', toNodeHandler(oAuthProtectedResourceMetadata(auth))); + + // Body parsers for non-better-auth routes (like /sign-in) + authApp.use(express.json()); + authApp.use(express.urlencoded({ extended: true })); + + // Auto-login page that creates a real better-auth session // This simulates a user logging in and approving the OAuth request authApp.get('/sign-in', async (req: Request, res: ExpressResponse) => { // Get the OAuth authorization parameters from the query string @@ -101,43 +165,52 @@ export function setupAuthServer(options: SetupAuthServerOptions): OAuthMetadata return; } - // For demo: auto-approve by redirecting to the authorization endpoint - // with a flag indicating auto-approval - // In better-auth, we need to create a session first, then complete authorization - - // Set a demo session cookie - const authCookieData = { - userId: 'demo_user', - name: 'Demo User', - timestamp: Date.now() - }; - const cookieValue = encodeURIComponent(JSON.stringify(authCookieData)); - res.cookie('demo_session', cookieValue, { - httpOnly: true, - sameSite: 'lax', - maxAge: 24 * 60 * 60 * 1000 // 24 hours - }); - - // Redirect to the actual authorization handler with auto-approve - // Better-auth handles the OAuth flow at /api/auth/authorize - const authorizeUrl = new URL('/api/auth/authorize', authServerUrl); - authorizeUrl.search = queryParams.toString(); - // Add a flag to indicate auto-approval (this would be handled by a custom flow) - authorizeUrl.searchParams.set('auto_approve', 'true'); - - console.log(`[Auth Server] Auto-approved login for client ${clientId}`); - res.redirect(authorizeUrl.toString()); + try { + // Ensure demo user exists (creates with proper password hash) + await ensureDemoUserExists(auth); + + // Create a session using better-auth's signIn API with asResponse to get Set-Cookie headers + const signInResponse = await auth.api.signInEmail({ + body: { + email: DEMO_USER_CREDENTIALS.email, + password: DEMO_USER_CREDENTIALS.password + }, + asResponse: true + }); + + console.log('[Auth] Sign-in response status:', signInResponse.status); + + // Forward all Set-Cookie headers from better-auth's response + const setCookieHeaders = signInResponse.headers.getSetCookie(); + console.log('[Auth] Set-Cookie headers:', setCookieHeaders); + + for (const cookie of setCookieHeaders) { + res.append('Set-Cookie', cookie); + } + + console.log(`[Auth Server] Session created, redirecting to authorize`); + + // Redirect to the authorization endpoint + const authorizeUrl = new URL('/api/auth/mcp/authorize', authServerUrl); + authorizeUrl.search = queryParams.toString(); + + res.redirect(authorizeUrl.toString()); + } catch (error) { + console.error('[Auth Server] Failed to create session:', error); + res.status(500).send(` + + + Demo Login Error + +

Demo OAuth Server - Error

+

Failed to create demo session: ${error instanceof Error ? error.message : 'Unknown error'}

+
${error instanceof Error ? error.stack : ''}
+ + + `); + } }); - // Mount better-auth handler for all /api/auth/* routes - // This handles: authorization, token, client registration, etc. - authApp.all('/api/auth/*', toNodeHandler(auth)); - - // OAuth metadata endpoints using better-auth's built-in handlers - // See: https://www.better-auth.com/docs/plugins/mcp#oauth-discovery-metadata - authApp.get('/.well-known/oauth-authorization-server', toNodeHandler(oAuthDiscoveryMetadata(auth))); - authApp.get('/.well-known/oauth-protected-resource', toNodeHandler(oAuthProtectedResourceMetadata(auth))); - // Start the auth server const authPort = parseInt(authServerUrl.port, 10); authApp.listen(authPort, (error?: Error) => { @@ -146,12 +219,10 @@ export function setupAuthServer(options: SetupAuthServerOptions): OAuthMetadata process.exit(1); } console.log(`OAuth Authorization Server listening on port ${authPort}`); - console.log(` Authorization: ${authServerUrl}api/auth/authorize`); - console.log(` Token: ${authServerUrl}api/auth/token`); + console.log(` Authorization: ${authServerUrl}api/auth/mcp/authorize`); + console.log(` Token: ${authServerUrl}api/auth/mcp/token`); console.log(` Metadata: ${authServerUrl}.well-known/oauth-authorization-server`); }); - - return createOAuthMetadata(authServerUrl); } /** @@ -173,27 +244,6 @@ export function createProtectedResourceMetadataRouter(): Router { return router; } -/** - * Creates OAuth 2.0 Authorization Server Metadata (RFC 8414) - */ -function createOAuthMetadata(issuerUrl: URL): OAuthMetadata { - const issuer = issuerUrl.toString().replace(/\/$/, ''); - const apiAuthBase = `${issuer}/api/auth`; - - return { - issuer, - authorization_endpoint: `${apiAuthBase}/authorize`, - token_endpoint: `${apiAuthBase}/token`, - registration_endpoint: `${apiAuthBase}/register`, - introspection_endpoint: `${apiAuthBase}/introspect`, - scopes_supported: ['openid', 'profile', 'email', 'offline_access', 'mcp:tools'], - response_types_supported: ['code'], - grant_types_supported: ['authorization_code', 'refresh_token'], - token_endpoint_auth_methods_supported: ['none', 'client_secret_post', 'client_secret_basic'], - code_challenge_methods_supported: ['S256'] - }; -} - /** * Verifies an access token using better-auth's getMcpSession. * This can be used by MCP servers to validate tokens. From 8dba772779bd75216f7124ab25acb036ac669075 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 22 Dec 2025 00:54:09 +0200 Subject: [PATCH 10/23] lint fix --- docs/faq.md | 3 +- examples/shared/src/authServer.ts | 6 +- examples/shared/src/index.ts | 2 +- pnpm-workspace.yaml | 102 +++++++++++++++--------------- 4 files changed, 57 insertions(+), 56 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 4e2f6cc35..b5e2025b2 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -71,7 +71,8 @@ Server authentication & authorization is outside of the scope of the SDK, and th ### Why did we remove `server` SSE transport? -The SSE transport has been deprecated for a long time, and `v2` will not support it on the server side any more. Client side will keep supporting it in order to be able to connect to legacy SSE servers via the `v2` SDK, but serving SSE from `v2` will not be possible. Servers wanting to switch to `v2` and using SSE should migrate to Streamable HTTP. +The SSE transport has been deprecated for a long time, and `v2` will not support it on the server side any more. Client side will keep supporting it in order to be able to connect to legacy SSE servers via the `v2` SDK, but serving SSE from `v2` will not be possible. Servers +wanting to switch to `v2` and using SSE should migrate to Streamable HTTP. ## v1 (legacy) diff --git a/examples/shared/src/authServer.ts b/examples/shared/src/authServer.ts index 7147f83e8..1a99146bc 100644 --- a/examples/shared/src/authServer.ts +++ b/examples/shared/src/authServer.ts @@ -110,10 +110,10 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { if (req.method === 'POST') { console.log(`${timestamp} [Auth Request] Content-Type: ${req.headers['content-type']}`); } - + // Log response when it finishes const originalSend = res.send.bind(res); - res.send = function(body) { + res.send = function (body) { console.log(`${timestamp} [Auth Response] ${res.statusCode} ${req.url}`); if (res.statusCode >= 400 && body) { try { @@ -183,7 +183,7 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { // Forward all Set-Cookie headers from better-auth's response const setCookieHeaders = signInResponse.headers.getSetCookie(); console.log('[Auth] Set-Cookie headers:', setCookieHeaders); - + for (const cookie of setCookieHeaders) { res.append('Set-Cookie', cookie); } diff --git a/examples/shared/src/index.ts b/examples/shared/src/index.ts index e353700bc..6a9873c11 100644 --- a/examples/shared/src/index.ts +++ b/examples/shared/src/index.ts @@ -7,5 +7,5 @@ export type { RequireBearerAuthOptions } from './authMiddleware.js'; export { getOAuthProtectedResourceMetadataUrl, requireBearerAuth } from './authMiddleware.js'; // Auth server setup -export type { AuthServerResult, SetupAuthServerOptions } from './authServer.js'; +export type { SetupAuthServerOptions } from './authServer.js'; export { createProtectedResourceMetadataRouter, getAuth, setupAuthServer, verifyAccessToken } from './authServer.js'; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0d86e1799..927d4566e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,60 +1,60 @@ packages: - - packages/**/* - - common/**/* - - examples/**/* - - test/**/* + - packages/**/* + - common/**/* + - examples/**/* + - test/**/* catalogs: - devTools: - '@eslint/js': ^9.39.1 - '@types/content-type': ^1.1.8 - '@types/cors': ^2.8.17 - '@types/cross-spawn': ^6.0.6 - '@types/eventsource': ^1.1.15 - '@types/express': ^5.0.0 - '@types/express-serve-static-core': ^5.1.0 - '@types/supertest': ^6.0.2 - '@types/ws': ^8.5.12 - '@typescript/native-preview': ^7.0.0-dev.20251217.1 - eslint: ^9.8.0 - eslint-config-prettier: ^10.1.8 - eslint-plugin-n: ^17.23.1 - prettier: 3.6.2 - supertest: ^7.0.0 - tsdown: ^0.18.0 - tsx: ^4.16.5 - typescript: ^5.9.3 - typescript-eslint: ^8.48.1 - vite-tsconfig-paths: ^5.1.4 - vitest: ^4.0.8 - ws: ^8.18.0 - runtimeClientOnly: - cross-spawn: ^7.0.5 - eventsource: ^3.0.2 - eventsource-parser: ^3.0.0 - jose: ^6.1.1 - runtimeServerOnly: - '@hono/node-server': ^1.19.7 - '@remix-run/node-fetch-server': ^0.13.0 - content-type: ^1.0.5 - cors: ^2.8.5 - express: ^5.0.1 - express-rate-limit: ^8.2.1 - hono: ^4.11.1 - raw-body: ^3.0.0 - runtimeShared: - '@cfworker/json-schema': ^4.1.1 - ajv: ^8.17.1 - ajv-formats: ^3.0.1 - json-schema-typed: ^8.0.2 - pkce-challenge: ^5.0.0 - zod: ^3.25 || ^4.0 - zod-to-json-schema: ^3.25.0 + devTools: + '@eslint/js': ^9.39.1 + '@types/content-type': ^1.1.8 + '@types/cors': ^2.8.17 + '@types/cross-spawn': ^6.0.6 + '@types/eventsource': ^1.1.15 + '@types/express': ^5.0.0 + '@types/express-serve-static-core': ^5.1.0 + '@types/supertest': ^6.0.2 + '@types/ws': ^8.5.12 + '@typescript/native-preview': ^7.0.0-dev.20251217.1 + eslint: ^9.8.0 + eslint-config-prettier: ^10.1.8 + eslint-plugin-n: ^17.23.1 + prettier: 3.6.2 + supertest: ^7.0.0 + tsdown: ^0.18.0 + tsx: ^4.16.5 + typescript: ^5.9.3 + typescript-eslint: ^8.48.1 + vite-tsconfig-paths: ^5.1.4 + vitest: ^4.0.8 + ws: ^8.18.0 + runtimeClientOnly: + cross-spawn: ^7.0.5 + eventsource: ^3.0.2 + eventsource-parser: ^3.0.0 + jose: ^6.1.1 + runtimeServerOnly: + '@hono/node-server': ^1.19.7 + '@remix-run/node-fetch-server': ^0.13.0 + content-type: ^1.0.5 + cors: ^2.8.5 + express: ^5.0.1 + express-rate-limit: ^8.2.1 + hono: ^4.11.1 + raw-body: ^3.0.0 + runtimeShared: + '@cfworker/json-schema': ^4.1.1 + ajv: ^8.17.1 + ajv-formats: ^3.0.1 + json-schema-typed: ^8.0.2 + pkce-challenge: ^5.0.0 + zod: ^3.25 || ^4.0 + zod-to-json-schema: ^3.25.0 enableGlobalVirtualStore: false linkWorkspacePackages: deep onlyBuiltDependencies: - - better-sqlite3 - - esbuild + - better-sqlite3 + - esbuild From 45f0ef11efcd524c6fe2c68bafac199ca4bc47a3 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 22 Dec 2025 01:04:15 +0200 Subject: [PATCH 11/23] clean up --- examples/shared/src/auth.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/shared/src/auth.ts b/examples/shared/src/auth.ts index e2c6aca54..830d88147 100644 --- a/examples/shared/src/auth.ts +++ b/examples/shared/src/auth.ts @@ -202,8 +202,11 @@ export function createDemoAuth(options: CreateDemoAuthOptions) { accessTokenExpiresIn: 3600, // 1 hour refreshTokenExpiresIn: 604800, // 7 days defaultScope: 'openid', - scopes: ['openid', 'profile', 'email', 'offline_access', 'mcp:tools'], - allowDynamicClientRegistration: true + scopes: ['openid', 'profile', 'email', 'offline_access'], + allowDynamicClientRegistration: true, + metadata: { + scopes_supported: ['openid', 'profile', 'email', 'offline_access'] + } } }); From f5ab824c49168aa31eb45b4708a7ab0aed13b2df Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 22 Dec 2025 03:31:58 +0200 Subject: [PATCH 12/23] package clean up --- package-lock.json | 4591 -------------------------- packages/server-express/package.json | 1 - pnpm-lock.yaml | 46 +- pnpm-workspace.yaml | 1 - 4 files changed, 13 insertions(+), 4626 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 64bc6f21d..000000000 --- a/package-lock.json +++ /dev/null @@ -1,4591 +0,0 @@ -{ - "name": "@modelcontextprotocol/sdk", - "version": "1.25.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@modelcontextprotocol/sdk", - "version": "1.25.1", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.7", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" - }, - "devDependencies": { - "@cfworker/json-schema": "^4.1.1", - "@eslint/js": "^9.39.1", - "@types/content-type": "^1.1.8", - "@types/cors": "^2.8.17", - "@types/cross-spawn": "^6.0.6", - "@types/eventsource": "^1.1.15", - "@types/express": "^5.0.0", - "@types/express-serve-static-core": "^5.1.0", - "@types/node": "^22.12.0", - "@types/supertest": "^6.0.2", - "@types/ws": "^8.5.12", - "@typescript/native-preview": "^7.0.0-dev.20251103.1", - "eslint": "^9.8.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-n": "^17.23.1", - "prettier": "3.6.2", - "supertest": "^7.0.0", - "tsx": "^4.16.5", - "typescript": "^5.5.4", - "typescript-eslint": "^8.48.1", - "vitest": "^4.0.8", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@cfworker/json-schema": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", - "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", - "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", - "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", - "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", - "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", - "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", - "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", - "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", - "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", - "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", - "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", - "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", - "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", - "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", - "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", - "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", - "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", - "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", - "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", - "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", - "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", - "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", - "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/content-type": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", - "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==", - "dev": true - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/eventsource": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", - "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==", - "dev": true - }, - "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", - "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/node": { - "version": "22.19.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", - "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, - "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", - "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/type-utils": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.48.1", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.1", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20251103.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251103.1.tgz", - "integrity": "sha512-Pcyltv+XIbaCoRaD3btY3qu+B1VzvEgNGlq1lM0O11QTPRLHyoEfvtLqyPKuSDgD90gDbtCPGUppVkpQouLBVQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsgo": "bin/tsgo.js" - }, - "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251103.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251103.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20251103.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251103.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20251103.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251103.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20251103.1" - } - }, - "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20251103.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251103.1.tgz", - "integrity": "sha512-yqUxUts3zpxy0x+Rk/9VC+ZiwzXTiuNpgLbhLAR1inFxuk0kTM8xoQERaIk+DUn6guEmRiCzOw23aJ9u6E+GfA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20251103.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251103.1.tgz", - "integrity": "sha512-jboMuar6TgvnnOZk8t/X2gZp4TUtsP9xtUnLEMEHRPWK3LFBJpjDFRUH70vOpW9hWIKYajlkF8JutclCPX5sBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20251103.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251103.1.tgz", - "integrity": "sha512-QY+0W9TPxHub8vSFjemo3txSpCNGw3LqnrLKKlGUIuLW+Ohproo+o7Fq21dksPQ4g0NDWY19qlm/36QhsXKRNQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20251103.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251103.1.tgz", - "integrity": "sha512-PZewTo76n2chP8o0Fwq2543jVVSY7aiZMBsapB82+w/XecFuCQtFRYNN02x6pjHeVjgv5fcWS3+LzHa1zv10qw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20251103.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251103.1.tgz", - "integrity": "sha512-wdFUmmz5XFUvWQ54l3f8ODah86b6Z4FnG9gndjOdYRY2FGDCOdmeoBqLHDiGUIzTHr5FMMyz2EfScN+qtUh4Dw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20251103.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251103.1.tgz", - "integrity": "sha512-A00+b8mbwJ4RFwXZN4vNcIBGZcdBCFm23lBhw8uaUgLY1Ot81FZvJE3YZcbRrZwEiyrwd3hAMdnDBWUwMA9YqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20251103.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251103.1.tgz", - "integrity": "sha512-25Pqk65M3fjQdsnwBLym5ALSdQlQAqHKrzZOkIs1uFKxIfZ5s9658Kjfj2fiMX5m3imk9IqzpP+fvKbgP1plIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vitest/expect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.8.tgz", - "integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.8", - "@vitest/utils": "4.0.8", - "chai": "^6.2.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.8.tgz", - "integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.8", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.8.tgz", - "integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.8", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", - "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", - "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.8", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-es-x": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", - "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/ota-meshi", - "https://opencollective.com/eslint" - ], - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.11.0", - "eslint-compat-utils": "^0.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-plugin-n": { - "version": "17.23.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.23.1.tgz", - "integrity": "sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.5.0", - "enhanced-resolve": "^5.17.1", - "eslint-plugin-es-x": "^7.8.0", - "get-tsconfig": "^4.8.1", - "globals": "^15.11.0", - "globrex": "^0.1.2", - "ignore": "^5.3.2", - "semver": "^7.6.3", - "ts-declaration-location": "^1.0.6" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": ">=8.23.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.2.tgz", - "integrity": "sha512-YolzkJNxsTL3tCJMWFxpxtG2sCjbZ4LQUBUrkdaJK0ub0p6lmJt+2+1SwhKjLc652lpH9L/79Ptez972H9tphw==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", - "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formidable": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", - "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "function-bind": "^1.1.2", - "get-proto": "^1.0.0", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.10.8", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.8.tgz", - "integrity": "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/jose": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.1.tgz", - "integrity": "sha512-GWSqjfOPf4cWOkBzw5THBjtGPhXKqYnfRBzh4Ni+ArTrQQ9unvmsA3oFLqaYKoKe5sjWmGu5wVKg9Ft1i+LQfg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/rollup": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", - "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.2", - "@rollup/rollup-android-arm64": "4.53.2", - "@rollup/rollup-darwin-arm64": "4.53.2", - "@rollup/rollup-darwin-x64": "4.53.2", - "@rollup/rollup-freebsd-arm64": "4.53.2", - "@rollup/rollup-freebsd-x64": "4.53.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", - "@rollup/rollup-linux-arm-musleabihf": "4.53.2", - "@rollup/rollup-linux-arm64-gnu": "4.53.2", - "@rollup/rollup-linux-arm64-musl": "4.53.2", - "@rollup/rollup-linux-loong64-gnu": "4.53.2", - "@rollup/rollup-linux-ppc64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-musl": "4.53.2", - "@rollup/rollup-linux-s390x-gnu": "4.53.2", - "@rollup/rollup-linux-x64-gnu": "4.53.2", - "@rollup/rollup-linux-x64-musl": "4.53.2", - "@rollup/rollup-openharmony-arm64": "4.53.2", - "@rollup/rollup-win32-arm64-msvc": "4.53.2", - "@rollup/rollup-win32-ia32-msvc": "4.53.2", - "@rollup/rollup-win32-x64-gnu": "4.53.2", - "@rollup/rollup-win32-x64-msvc": "4.53.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/superagent": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", - "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^3.5.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supertest": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", - "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^9.0.1" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-declaration-location": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", - "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", - "dev": true, - "funding": [ - { - "type": "ko-fi", - "url": "https://ko-fi.com/rebeccastevens" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" - } - ], - "license": "BSD-3-Clause", - "dependencies": { - "picomatch": "^4.0.2" - }, - "peerDependencies": { - "typescript": ">=4.0.0" - } - }, - "node_modules/ts-declaration-location/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tsx": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", - "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", - "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.1", - "@typescript-eslint/parser": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vitest": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.8.tgz", - "integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.0.8", - "@vitest/mocker": "4.0.8", - "@vitest/pretty-format": "4.0.8", - "@vitest/runner": "4.0.8", - "@vitest/snapshot": "4.0.8", - "@vitest/spy": "4.0.8", - "@vitest/utils": "4.0.8", - "debug": "^4.4.3", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^3.10.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.8", - "@vitest/browser-preview": "4.0.8", - "@vitest/browser-webdriverio": "4.0.8", - "@vitest/ui": "4.0.8", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", - "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.0.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest/node_modules/vite": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", - "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", - "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } - } - } -} diff --git a/packages/server-express/package.json b/packages/server-express/package.json index bca9ac505..70fffa767 100644 --- a/packages/server-express/package.json +++ b/packages/server-express/package.json @@ -45,7 +45,6 @@ "dependencies": { "@modelcontextprotocol/server": "workspace:^", "express": "catalog:runtimeServerOnly", - "express-rate-limit": "catalog:runtimeServerOnly", "@remix-run/node-fetch-server": "catalog:runtimeServerOnly" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c249da48..9ac18b8ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,9 +101,6 @@ catalogs: express: specifier: ^5.0.1 version: 5.1.0 - express-rate-limit: - specifier: ^8.2.1 - version: 8.2.1 hono: specifier: ^4.11.1 version: 4.11.1 @@ -236,7 +233,7 @@ importers: version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + version: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) eslint-plugin-n: specifier: catalog:devTools version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) @@ -688,9 +685,6 @@ importers: express: specifier: catalog:runtimeServerOnly version: 5.1.0 - express-rate-limit: - specifier: catalog:runtimeServerOnly - version: 8.2.1(express@5.1.0) devDependencies: '@eslint/js': specifier: catalog:devTools @@ -2378,12 +2372,6 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.2.1: - resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} @@ -2637,10 +2625,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ip-address@10.0.1: - resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} - engines: {node: '>= 12'} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3738,7 +3722,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)': + '@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -3749,9 +3733,9 @@ snapshots: nanostores: 1.1.0 zod: 4.2.1 - '@better-auth/telemetry@1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))': + '@better-auth/telemetry@1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))': dependencies: - '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -4714,8 +4698,8 @@ snapshots: better-auth@1.4.7(better-sqlite3@11.10.0)(vitest@4.0.9(@types/node@24.10.3)(tsx@4.20.6)): dependencies: - '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.2.1))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) + '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -5118,14 +5102,15 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) eslint: 9.39.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1) @@ -5139,7 +5124,7 @@ snapshots: eslint: 9.39.1 eslint-compat-utils: 0.5.1(eslint@9.39.1) - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5150,7 +5135,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5161,6 +5146,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5269,11 +5256,6 @@ snapshots: expect-type@1.2.2: {} - express-rate-limit@8.2.1(express@5.1.0): - dependencies: - express: 5.1.0 - ip-address: 10.0.1 - express@5.1.0: dependencies: accepts: 2.0.0 @@ -5557,8 +5539,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - ip-address@10.0.1: {} - ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 927d4566e..26d63e39e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,7 +39,6 @@ catalogs: content-type: ^1.0.5 cors: ^2.8.5 express: ^5.0.1 - express-rate-limit: ^8.2.1 hono: ^4.11.1 raw-body: ^3.0.0 runtimeShared: From 71297bcf3eac0f46f2471dfb84e88392b815a569 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 1 Jan 2026 22:45:59 +0200 Subject: [PATCH 13/23] update express to 5.2.1 --- pnpm-lock.yaml | 35 ++++++++++++++--------------------- pnpm-workspace.yaml | 2 +- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ac18b8ed..a7aa6bf29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,8 +99,8 @@ catalogs: specifier: ^2.8.5 version: 2.8.5 express: - specifier: ^5.0.1 - version: 5.1.0 + specifier: ^5.2.1 + version: 5.2.1 hono: specifier: ^4.11.1 version: 4.11.1 @@ -319,7 +319,7 @@ importers: version: 2.8.5 express: specifier: catalog:runtimeServerOnly - version: 5.1.0 + version: 5.2.1 hono: specifier: catalog:runtimeServerOnly version: 4.11.1 @@ -365,7 +365,7 @@ importers: version: 11.10.0 express: specifier: catalog:runtimeServerOnly - version: 5.1.0 + version: 5.2.1 devDependencies: '@eslint/js': specifier: catalog:devTools @@ -684,7 +684,7 @@ importers: version: 0.13.0 express: specifier: catalog:runtimeServerOnly - version: 5.1.0 + version: 5.2.1 devDependencies: '@eslint/js': specifier: catalog:devTools @@ -1951,8 +1951,8 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} engines: {node: '>=18'} brace-expansion@1.1.12: @@ -2372,8 +2372,8 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} extendable-error@0.1.7: @@ -2584,10 +2584,6 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} @@ -4744,13 +4740,13 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@2.2.0: + body-parser@2.2.1: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.7.0 on-finished: 2.4.1 qs: 6.14.0 raw-body: 3.0.1 @@ -5256,15 +5252,16 @@ snapshots: expect-type@1.2.2: {} - express@5.1.0: + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 + body-parser: 2.2.1 content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 debug: 4.4.3 + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -5506,10 +5503,6 @@ snapshots: human-id@4.1.3: {} - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 26d63e39e..1acb89261 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -38,7 +38,7 @@ catalogs: '@remix-run/node-fetch-server': ^0.13.0 content-type: ^1.0.5 cors: ^2.8.5 - express: ^5.0.1 + express: ^5.2.1 hono: ^4.11.1 raw-body: ^3.0.0 runtimeShared: From 6c21c50981cd97a66aae124af3815f7ecb55d8a1 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 12 Jan 2026 17:14:03 +0200 Subject: [PATCH 14/23] merge commit --- pnpm-lock.yaml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ac1c85c6..c37a8e0fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2399,6 +2399,12 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -4124,7 +4130,7 @@ snapshots: '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.1)(zod@3.25.76) commander: 14.0.2 eventsource-parser: 3.0.6 - express: 5.1.0 + express: 5.2.1 jose: 6.1.3 zod: 3.25.76 transitivePeerDependencies: @@ -4142,8 +4148,8 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.0.6 - express: 5.1.0 - express-rate-limit: 7.5.1(express@5.1.0) + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 @@ -5318,6 +5324,10 @@ snapshots: expect-type@1.2.2: {} + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + express@5.2.1: dependencies: accepts: 2.0.0 From 304d111d85acd32ba5bf3d73d0103959c10daa5e Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 12 Jan 2026 17:56:37 +0200 Subject: [PATCH 15/23] split @modelcontextprotocol/node from @modelcontextprotocol/server to middleware folder, move express and hono to middleware folder --- .github/workflows/publish.yml | 2 +- docs/server.md | 4 +- examples/server/README.md | 4 +- examples/server/package.json | 4 +- examples/server/src/elicitationFormExample.ts | 2 +- examples/server/src/elicitationUrlExample.ts | 2 +- .../server/src/jsonResponseStreamableHttp.ts | 2 +- .../src/simpleStatelessStreamableHttp.ts | 2 +- examples/server/src/simpleStreamableHttp.ts | 2 +- examples/server/src/simpleTaskInteractive.ts | 2 +- examples/server/src/ssePollingExample.ts | 2 +- .../src/standaloneSseWithGetStreamableHttp.ts | 2 +- examples/server/tsconfig.json | 4 +- examples/shared/package.json | 4 +- examples/shared/src/auth.ts | 2 +- examples/shared/tsconfig.json | 2 +- package.json | 2 +- packages/client/package.json | 5 +- packages/core/package.json | 5 +- packages/core/test/shared/protocol.test.ts | 10 +- .../express}/README.md | 12 +- .../express}/eslint.config.mjs | 0 .../express}/package.json | 7 +- .../express}/src/express.ts | 0 .../express}/src/index.ts | 0 .../src/middleware/hostHeaderValidation.ts | 0 .../express}/test/server-express.test.ts | 2 +- .../express}/tsconfig.json | 0 .../express}/tsdown.config.ts | 0 .../express}/vitest.config.js | 0 .../hono}/README.md | 10 +- .../hono}/eslint.config.mjs | 0 .../hono}/package.json | 7 +- .../hono}/src/hono.ts | 0 .../hono}/src/index.ts | 0 .../src/middleware/hostHeaderValidation.ts | 0 .../hono}/test/server-hono.test.ts | 2 +- .../hono}/tsconfig.json | 0 .../hono}/tsdown.config.ts | 0 .../hono}/vitest.config.js | 0 packages/middleware/node/eslint.config.mjs | 12 + packages/middleware/node/package.json | 82 + packages/middleware/node/src/index.ts | 1 + .../middleware/node/src/streamableHttp.ts | 188 + .../node/test/streamableHttp.test.ts | 2987 +++++++++++++++ packages/middleware/node/tsconfig.json | 13 + packages/middleware/node/tsdown.config.ts | 34 + packages/middleware/node/vitest.config.js | 3 + packages/server/package.json | 14 +- packages/server/src/index.ts | 2 +- packages/server/src/server/streamableHttp.ts | 996 ++++- .../src/server/webStandardStreamableHttp.ts | 999 ----- .../test/server/__fixtures__/zodTestMatrix.ts | 22 - .../server/test/server/completable.test.ts | 6 +- .../server/test/server/streamableHttp.test.ts | 3204 +++-------------- packages/server/tsconfig.json | 2 +- pnpm-lock.yaml | 2086 ++++++----- pnpm-workspace.yaml | 4 +- test/integration/package.json | 2 +- test/integration/test/server.test.ts | 2 +- test/integration/test/server/mcp.test.ts | 9 +- test/integration/tsconfig.json | 2 +- 62 files changed, 5948 insertions(+), 4826 deletions(-) rename packages/{server-express => middleware/express}/README.md (82%) rename packages/{server-express => middleware/express}/eslint.config.mjs (100%) rename packages/{server-express => middleware/express}/package.json (94%) rename packages/{server-express => middleware/express}/src/express.ts (100%) rename packages/{server-express => middleware/express}/src/index.ts (100%) rename packages/{server-express => middleware/express}/src/middleware/hostHeaderValidation.ts (100%) rename packages/{server-express => middleware/express}/test/server-express.test.ts (99%) rename packages/{server-express => middleware/express}/tsconfig.json (100%) rename packages/{server-express => middleware/express}/tsdown.config.ts (100%) rename packages/{server-express => middleware/express}/vitest.config.js (100%) rename packages/{server-hono => middleware/hono}/README.md (84%) rename packages/{server-hono => middleware/hono}/eslint.config.mjs (100%) rename packages/{server-hono => middleware/hono}/package.json (94%) rename packages/{server-hono => middleware/hono}/src/hono.ts (100%) rename packages/{server-hono => middleware/hono}/src/index.ts (100%) rename packages/{server-hono => middleware/hono}/src/middleware/hostHeaderValidation.ts (100%) rename packages/{server-hono => middleware/hono}/test/server-hono.test.ts (98%) rename packages/{server-hono => middleware/hono}/tsconfig.json (100%) rename packages/{server-hono => middleware/hono}/tsdown.config.ts (100%) rename packages/{server-hono => middleware/hono}/vitest.config.js (100%) create mode 100644 packages/middleware/node/eslint.config.mjs create mode 100644 packages/middleware/node/package.json create mode 100644 packages/middleware/node/src/index.ts create mode 100644 packages/middleware/node/src/streamableHttp.ts create mode 100644 packages/middleware/node/test/streamableHttp.test.ts create mode 100644 packages/middleware/node/tsconfig.json create mode 100644 packages/middleware/node/tsdown.config.ts create mode 100644 packages/middleware/node/vitest.config.js delete mode 100644 packages/server/src/server/webStandardStreamableHttp.ts delete mode 100644 packages/server/test/server/__fixtures__/zodTestMatrix.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 25cd12862..a180396b6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -40,4 +40,4 @@ jobs: - name: Publish preview packages run: pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client' - './packages/server-express' './packages/server-hono' + './packages/middleware/express' './packages/middleware/hono' './packages/middleware/node' diff --git a/docs/server.md b/docs/server.md index 800d336db..ee05077ff 100644 --- a/docs/server.md +++ b/docs/server.md @@ -70,7 +70,7 @@ For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` to create an Express app with DNS rebinding protection enabled by default: ```typescript -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; // Protection auto-enabled (default host is 127.0.0.1) const app = createMcpExpressApp(); @@ -85,7 +85,7 @@ const app = createMcpExpressApp({ host: '0.0.0.0' }); When binding to `0.0.0.0` / `::`, provide an allow-list of hosts: ```typescript -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; const app = createMcpExpressApp({ host: '0.0.0.0', diff --git a/examples/server/README.md b/examples/server/README.md index 1e7322b1a..bfa67fd53 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -2,8 +2,8 @@ This directory contains runnable MCP **server** examples built with `@modelcontextprotocol/server` plus framework adapters: -- `@modelcontextprotocol/server-express` -- `@modelcontextprotocol/server-hono` +- `@modelcontextprotocol/express` +- `@modelcontextprotocol/hono` For client examples, see [`../client/README.md`](../client/README.md). For guided docs, see [`../../docs/server.md`](../../docs/server.md). diff --git a/examples/server/package.json b/examples/server/package.json index 7241f3ab2..e36dc3889 100644 --- a/examples/server/package.json +++ b/examples/server/package.json @@ -37,8 +37,8 @@ "@hono/node-server": "catalog:runtimeServerOnly", "@modelcontextprotocol/examples-shared": "workspace:^", "@modelcontextprotocol/server": "workspace:^", - "@modelcontextprotocol/server-express": "workspace:^", - "@modelcontextprotocol/server-hono": "workspace:^", + "@modelcontextprotocol/express": "workspace:^", + "@modelcontextprotocol/hono": "workspace:^", "better-auth": "^1.4.7", "cors": "catalog:runtimeServerOnly", "express": "catalog:runtimeServerOnly", diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index eaeb73c32..5f82532e9 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -9,8 +9,8 @@ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; // Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index 39a591708..194788ae4 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -15,6 +15,7 @@ import { requireBearerAuth, setupAuthServer } from '@modelcontextprotocol/examples-shared'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import type { CallToolResult, ElicitRequestURLParams, ElicitResult } from '@modelcontextprotocol/server'; import { isInitializeRequest, @@ -22,7 +23,6 @@ import { NodeStreamableHTTPServerTransport, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; diff --git a/examples/server/src/jsonResponseStreamableHttp.ts b/examples/server/src/jsonResponseStreamableHttp.ts index 5935ad2c2..6ff679f39 100644 --- a/examples/server/src/jsonResponseStreamableHttp.ts +++ b/examples/server/src/jsonResponseStreamableHttp.ts @@ -1,8 +1,8 @@ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import type { CallToolResult } from '@modelcontextprotocol/server'; import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleStatelessStreamableHttp.ts b/examples/server/src/simpleStatelessStreamableHttp.ts index 70389275c..30ed060b8 100644 --- a/examples/server/src/simpleStatelessStreamableHttp.ts +++ b/examples/server/src/simpleStatelessStreamableHttp.ts @@ -1,6 +1,6 @@ +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import type { CallToolResult, GetPromptResult, ReadResourceResult } from '@modelcontextprotocol/server'; import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index b62b992f1..5e5e19570 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -6,6 +6,7 @@ import { requireBearerAuth, setupAuthServer } from '@modelcontextprotocol/examples-shared'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import type { CallToolResult, GetPromptResult, @@ -21,7 +22,6 @@ import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index 469ecf0c2..a9378478c 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -11,6 +11,7 @@ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import type { CallToolResult, CreateMessageRequest, @@ -44,7 +45,6 @@ import { RELATED_TASK_META_KEY, Server } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; // ============================================================================ diff --git a/examples/server/src/ssePollingExample.ts b/examples/server/src/ssePollingExample.ts index 4d0841dee..5d0cca842 100644 --- a/examples/server/src/ssePollingExample.ts +++ b/examples/server/src/ssePollingExample.ts @@ -14,9 +14,9 @@ */ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import type { CallToolResult } from '@modelcontextprotocol/server'; import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import cors from 'cors'; import type { Request, Response } from 'express'; diff --git a/examples/server/src/standaloneSseWithGetStreamableHttp.ts b/examples/server/src/standaloneSseWithGetStreamableHttp.ts index 869d7e859..af7bb0e04 100644 --- a/examples/server/src/standaloneSseWithGetStreamableHttp.ts +++ b/examples/server/src/standaloneSseWithGetStreamableHttp.ts @@ -1,8 +1,8 @@ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import type { ReadResourceResult } from '@modelcontextprotocol/server'; import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; // Create an MCP server with implementation details diff --git a/examples/server/tsconfig.json b/examples/server/tsconfig.json index 1f72b0199..21582de6d 100644 --- a/examples/server/tsconfig.json +++ b/examples/server/tsconfig.json @@ -6,8 +6,8 @@ "paths": { "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], - "@modelcontextprotocol/server-express": ["./node_modules/@modelcontextprotocol/server-express/src/index.ts"], - "@modelcontextprotocol/server-hono": ["./node_modules/@modelcontextprotocol/server-hono/src/index.ts"], + "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], + "@modelcontextprotocol/hono": ["./node_modules/@modelcontextprotocol/hono/src/index.ts"], "@modelcontextprotocol/core": [ "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" ], diff --git a/examples/shared/package.json b/examples/shared/package.json index aad10e3f6..1561b6e04 100644 --- a/examples/shared/package.json +++ b/examples/shared/package.json @@ -36,9 +36,9 @@ "dependencies": { "@modelcontextprotocol/core": "workspace:^", "@modelcontextprotocol/server": "workspace:^", - "@modelcontextprotocol/server-express": "workspace:^", + "@modelcontextprotocol/express": "workspace:^", "better-auth": "^1.4.7", - "better-sqlite3": "^11.10.0", + "better-sqlite3": "^12.4.1", "express": "catalog:runtimeServerOnly" }, "devDependencies": { diff --git a/examples/shared/src/auth.ts b/examples/shared/src/auth.ts index 830d88147..4813ce786 100644 --- a/examples/shared/src/auth.ts +++ b/examples/shared/src/auth.ts @@ -185,7 +185,7 @@ export interface CreateDemoAuthOptions { * * @see https://www.better-auth.com/docs/plugins/mcp */ -export function createDemoAuth(options: CreateDemoAuthOptions) { +export function createDemoAuth(options: CreateDemoAuthOptions): ReturnType { const { baseURL, resource, loginPage = '/sign-in' } = options; // Use in-memory SQLite database for demo purposes diff --git a/examples/shared/tsconfig.json b/examples/shared/tsconfig.json index 91e368e7a..69d2f966a 100644 --- a/examples/shared/tsconfig.json +++ b/examples/shared/tsconfig.json @@ -6,7 +6,7 @@ "paths": { "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], - "@modelcontextprotocol/server-express": ["./node_modules/@modelcontextprotocol/server-express/src/index.ts"], + "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/core": [ "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" ], diff --git a/package.json b/package.json index 35bde233d..3757f3b54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.25.1", + "version": "2.0.0-alpha.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/packages/client/package.json b/packages/client/package.json index e62a5bf16..0b02a42e8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,7 +1,7 @@ { "name": "@modelcontextprotocol/client", "version": "2.0.0-alpha.0", - "description": "Model Context Protocol implementation for TypeScript", + "description": "Model Context Protocol implementation for TypeScript - Client package", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", "homepage": "https://modelcontextprotocol.io", @@ -18,7 +18,8 @@ "packageManager": "pnpm@10.24.0", "keywords": [ "modelcontextprotocol", - "mcp" + "mcp", + "client" ], "exports": { ".": { diff --git a/packages/core/package.json b/packages/core/package.json index a7364d4dd..33bfe54ed 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,7 +2,7 @@ "name": "@modelcontextprotocol/core", "private": true, "version": "2.0.0-alpha.0", - "description": "Model Context Protocol implementation for TypeScript", + "description": "Model Context Protocol implementation for TypeScript - Core package", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", "homepage": "https://modelcontextprotocol.io", @@ -19,7 +19,8 @@ "packageManager": "pnpm@10.24.0", "keywords": [ "modelcontextprotocol", - "mcp" + "mcp", + "core" ], "scripts": { "typecheck": "tsgo -p tsconfig.json --noEmit", diff --git a/packages/core/test/shared/protocol.test.ts b/packages/core/test/shared/protocol.test.ts index b16a4453d..9f3ca111e 100644 --- a/packages/core/test/shared/protocol.test.ts +++ b/packages/core/test/shared/protocol.test.ts @@ -2095,7 +2095,7 @@ describe('Request Cancellation vs Task Cancellation', () => { let wasAborted = false; const TestRequestSchema = z.object({ method: z.literal('test/longRunning'), - params: z.optional(z.record(z.unknown())) + params: z.optional(z.record(z.string(), z.unknown())) }); protocol.setRequestHandler(TestRequestSchema, async (_request, extra) => { // Simulate a long-running operation @@ -2310,7 +2310,7 @@ describe('Request Cancellation vs Task Cancellation', () => { let requestCompleted = false; const TestMethodSchema = z.object({ method: z.literal('test/method'), - params: z.optional(z.record(z.unknown())) + params: z.optional(z.record(z.string(), z.unknown())) }); protocol.setRequestHandler(TestMethodSchema, async () => { await new Promise(resolve => setTimeout(resolve, 50)); @@ -3690,7 +3690,7 @@ describe('Message Interception', () => { method: z.literal('test/taskRequest'), params: z .object({ - _meta: z.optional(z.record(z.unknown())) + _meta: z.optional(z.record(z.string(), z.unknown())) }) .passthrough() }); @@ -3737,7 +3737,7 @@ describe('Message Interception', () => { method: z.literal('test/taskRequestError'), params: z .object({ - _meta: z.optional(z.record(z.unknown())) + _meta: z.optional(z.record(z.string(), z.unknown())) }) .passthrough() }); @@ -3817,7 +3817,7 @@ describe('Message Interception', () => { // Set up a request handler const TestRequestSchema = z.object({ method: z.literal('test/normalRequest'), - params: z.optional(z.record(z.unknown())) + params: z.optional(z.record(z.string(), z.unknown())) }); protocol.setRequestHandler(TestRequestSchema, async () => { diff --git a/packages/server-express/README.md b/packages/middleware/express/README.md similarity index 82% rename from packages/server-express/README.md rename to packages/middleware/express/README.md index 27fb348d7..6191149ae 100644 --- a/packages/server-express/README.md +++ b/packages/middleware/express/README.md @@ -1,4 +1,4 @@ -# `@modelcontextprotocol/server-express` +# `@modelcontextprotocol/express` Express adapters for the MCP TypeScript server SDK. @@ -7,7 +7,7 @@ This package is the Express-specific companion to [`@modelcontextprotocol/server ## Install ```bash -npm install @modelcontextprotocol/server @modelcontextprotocol/server-express zod +npm install @modelcontextprotocol/server @modelcontextprotocol/express zod ``` ## Exports @@ -24,7 +24,7 @@ npm install @modelcontextprotocol/server @modelcontextprotocol/server-express zo ### Create an Express app with localhost DNS rebinding protection ```ts -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; const app = createMcpExpressApp(); // default host is 127.0.0.1; protection enabled ``` @@ -33,7 +33,7 @@ const app = createMcpExpressApp(); // default host is 127.0.0.1; protection enab ```ts import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; const app = createMcpExpressApp(); @@ -48,7 +48,7 @@ app.post('/mcp', async (req, res) => { `@modelcontextprotocol/server` provides Web-standard auth handlers; this package wraps them as Express routers. ```ts -import { mcpAuthRouter } from '@modelcontextprotocol/server-express'; +import { mcpAuthRouter } from '@modelcontextprotocol/express'; import type { OAuthServerProvider } from '@modelcontextprotocol/server'; import express from 'express'; @@ -72,7 +72,7 @@ app.use( `requireBearerAuth` validates the `Authorization: Bearer ...` header and sets `req.auth` on success. ```ts -import { requireBearerAuth } from '@modelcontextprotocol/server-express'; +import { requireBearerAuth } from '@modelcontextprotocol/express'; import type { OAuthTokenVerifier } from '@modelcontextprotocol/server'; const verifier: OAuthTokenVerifier = /* ... */; diff --git a/packages/server-express/eslint.config.mjs b/packages/middleware/express/eslint.config.mjs similarity index 100% rename from packages/server-express/eslint.config.mjs rename to packages/middleware/express/eslint.config.mjs diff --git a/packages/server-express/package.json b/packages/middleware/express/package.json similarity index 94% rename from packages/server-express/package.json rename to packages/middleware/express/package.json index 70fffa767..5185ceffa 100644 --- a/packages/server-express/package.json +++ b/packages/middleware/express/package.json @@ -1,8 +1,8 @@ { - "name": "@modelcontextprotocol/server-express", + "name": "@modelcontextprotocol/express", "private": false, "version": "2.0.0-alpha.0", - "description": "Express adapters for the Model Context Protocol TypeScript server SDK", + "description": "Express adapters for the Model Context Protocol TypeScript server SDK - Express middleware", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", "homepage": "https://modelcontextprotocol.io", @@ -20,7 +20,8 @@ "keywords": [ "modelcontextprotocol", "mcp", - "express" + "express", + "middleware" ], "exports": { ".": { diff --git a/packages/server-express/src/express.ts b/packages/middleware/express/src/express.ts similarity index 100% rename from packages/server-express/src/express.ts rename to packages/middleware/express/src/express.ts diff --git a/packages/server-express/src/index.ts b/packages/middleware/express/src/index.ts similarity index 100% rename from packages/server-express/src/index.ts rename to packages/middleware/express/src/index.ts diff --git a/packages/server-express/src/middleware/hostHeaderValidation.ts b/packages/middleware/express/src/middleware/hostHeaderValidation.ts similarity index 100% rename from packages/server-express/src/middleware/hostHeaderValidation.ts rename to packages/middleware/express/src/middleware/hostHeaderValidation.ts diff --git a/packages/server-express/test/server-express.test.ts b/packages/middleware/express/test/server-express.test.ts similarity index 99% rename from packages/server-express/test/server-express.test.ts rename to packages/middleware/express/test/server-express.test.ts index 9bed5903a..528cbbf17 100644 --- a/packages/server-express/test/server-express.test.ts +++ b/packages/middleware/express/test/server-express.test.ts @@ -22,7 +22,7 @@ function createMockReqResNext(host?: string) { return { req, res, next }; } -describe('@modelcontextprotocol/server-express', () => { +describe('@modelcontextprotocol/express', () => { describe('hostHeaderValidation', () => { test('should block invalid Host header', () => { const middleware = hostHeaderValidation(['localhost']); diff --git a/packages/server-express/tsconfig.json b/packages/middleware/express/tsconfig.json similarity index 100% rename from packages/server-express/tsconfig.json rename to packages/middleware/express/tsconfig.json diff --git a/packages/server-express/tsdown.config.ts b/packages/middleware/express/tsdown.config.ts similarity index 100% rename from packages/server-express/tsdown.config.ts rename to packages/middleware/express/tsdown.config.ts diff --git a/packages/server-express/vitest.config.js b/packages/middleware/express/vitest.config.js similarity index 100% rename from packages/server-express/vitest.config.js rename to packages/middleware/express/vitest.config.js diff --git a/packages/server-hono/README.md b/packages/middleware/hono/README.md similarity index 84% rename from packages/server-hono/README.md rename to packages/middleware/hono/README.md index d2788881a..5e6ea5831 100644 --- a/packages/server-hono/README.md +++ b/packages/middleware/hono/README.md @@ -1,4 +1,4 @@ -# `@modelcontextprotocol/server-hono` +# `@modelcontextprotocol/hono` Hono adapters for the MCP TypeScript server SDK. @@ -7,7 +7,7 @@ This package is the Hono-specific companion to [`@modelcontextprotocol/server`]( ## Install ```bash -npm install @modelcontextprotocol/server @modelcontextprotocol/server-hono hono zod +npm install @modelcontextprotocol/server @modelcontextprotocol/hono hono zod ``` ## Exports @@ -25,7 +25,7 @@ npm install @modelcontextprotocol/server @modelcontextprotocol/server-hono hono ```ts import { Hono } from 'hono'; import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { mcpStreamableHttpHandler } from '@modelcontextprotocol/server-hono'; +import { mcpStreamableHttpHandler } from '@modelcontextprotocol/hono'; const server = new McpServer({ name: 'my-server', version: '1.0.0' }); const transport = new WebStandardStreamableHTTPServerTransport(); @@ -42,7 +42,7 @@ app.all('/mcp', mcpStreamableHttpHandler(transport)); ```ts import { Hono } from 'hono'; import type { OAuthServerProvider } from '@modelcontextprotocol/server'; -import { registerMcpAuthRoutes } from '@modelcontextprotocol/server-hono'; +import { registerMcpAuthRoutes } from '@modelcontextprotocol/hono'; const provider: OAuthServerProvider = /* ... */; @@ -57,7 +57,7 @@ registerMcpAuthRoutes(app, { ```ts import { Hono } from 'hono'; -import { localhostHostValidation } from '@modelcontextprotocol/server-hono'; +import { localhostHostValidation } from '@modelcontextprotocol/hono'; const app = new Hono(); app.use('*', localhostHostValidation()); diff --git a/packages/server-hono/eslint.config.mjs b/packages/middleware/hono/eslint.config.mjs similarity index 100% rename from packages/server-hono/eslint.config.mjs rename to packages/middleware/hono/eslint.config.mjs diff --git a/packages/server-hono/package.json b/packages/middleware/hono/package.json similarity index 94% rename from packages/server-hono/package.json rename to packages/middleware/hono/package.json index ac5b01a89..255b82b49 100644 --- a/packages/server-hono/package.json +++ b/packages/middleware/hono/package.json @@ -1,8 +1,8 @@ { - "name": "@modelcontextprotocol/server-hono", + "name": "@modelcontextprotocol/hono", "private": false, "version": "2.0.0-alpha.0", - "description": "Hono adapters for the Model Context Protocol TypeScript server SDK", + "description": "Hono adapters for the Model Context Protocol TypeScript server SDK - Hono middleware", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", "homepage": "https://modelcontextprotocol.io", @@ -20,7 +20,8 @@ "keywords": [ "modelcontextprotocol", "mcp", - "hono" + "hono", + "middleware" ], "exports": { ".": { diff --git a/packages/server-hono/src/hono.ts b/packages/middleware/hono/src/hono.ts similarity index 100% rename from packages/server-hono/src/hono.ts rename to packages/middleware/hono/src/hono.ts diff --git a/packages/server-hono/src/index.ts b/packages/middleware/hono/src/index.ts similarity index 100% rename from packages/server-hono/src/index.ts rename to packages/middleware/hono/src/index.ts diff --git a/packages/server-hono/src/middleware/hostHeaderValidation.ts b/packages/middleware/hono/src/middleware/hostHeaderValidation.ts similarity index 100% rename from packages/server-hono/src/middleware/hostHeaderValidation.ts rename to packages/middleware/hono/src/middleware/hostHeaderValidation.ts diff --git a/packages/server-hono/test/server-hono.test.ts b/packages/middleware/hono/test/server-hono.test.ts similarity index 98% rename from packages/server-hono/test/server-hono.test.ts rename to packages/middleware/hono/test/server-hono.test.ts index 230a566ba..c9d146340 100644 --- a/packages/server-hono/test/server-hono.test.ts +++ b/packages/middleware/hono/test/server-hono.test.ts @@ -5,7 +5,7 @@ import { vi } from 'vitest'; import { createMcpHonoApp } from '../src/hono.js'; import { hostHeaderValidation } from '../src/middleware/hostHeaderValidation.js'; -describe('@modelcontextprotocol/server-hono', () => { +describe('@modelcontextprotocol/hono', () => { test('hostHeaderValidation blocks invalid Host and allows valid Host', async () => { const app = new Hono(); app.use('*', hostHeaderValidation(['localhost'])); diff --git a/packages/server-hono/tsconfig.json b/packages/middleware/hono/tsconfig.json similarity index 100% rename from packages/server-hono/tsconfig.json rename to packages/middleware/hono/tsconfig.json diff --git a/packages/server-hono/tsdown.config.ts b/packages/middleware/hono/tsdown.config.ts similarity index 100% rename from packages/server-hono/tsdown.config.ts rename to packages/middleware/hono/tsdown.config.ts diff --git a/packages/server-hono/vitest.config.js b/packages/middleware/hono/vitest.config.js similarity index 100% rename from packages/server-hono/vitest.config.js rename to packages/middleware/hono/vitest.config.js diff --git a/packages/middleware/node/eslint.config.mjs b/packages/middleware/node/eslint.config.mjs new file mode 100644 index 000000000..4f034f223 --- /dev/null +++ b/packages/middleware/node/eslint.config.mjs @@ -0,0 +1,12 @@ +// @ts-check + +import baseConfig from '@modelcontextprotocol/eslint-config'; + +export default [ + ...baseConfig, + { + settings: { + 'import/internal-regex': '^@modelcontextprotocol/core' + } + } +]; diff --git a/packages/middleware/node/package.json b/packages/middleware/node/package.json new file mode 100644 index 000000000..7a93055d7 --- /dev/null +++ b/packages/middleware/node/package.json @@ -0,0 +1,82 @@ +{ + "name": "@modelcontextprotocol/node", + "version": "2.0.0-alpha.0", + "description": "Model Context Protocol implementation for TypeScript - Node.js middleware", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=20", + "pnpm": ">=10.24.0" + }, + "packageManager": "pnpm@10.24.0", + "keywords": [ + "modelcontextprotocol", + "mcp", + "node.js", + "middleware" + ], + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", + "build": "tsdown", + "build:watch": "tsdown --watch", + "prepack": "npm run build", + "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", + "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", + "check": "npm run typecheck && npm run lint", + "test": "vitest run", + "test:watch": "vitest", + "start": "npm run server", + "server": "tsx watch --clear-screen=false scripts/cli.ts server", + "client": "tsx scripts/cli.ts client" + }, + "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", + "content-type": "catalog:runtimeServerOnly", + "raw-body": "catalog:runtimeServerOnly" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "workspace:^" + }, + "devDependencies": { + "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/eslint-config": "workspace:^", + "@modelcontextprotocol/test-helpers": "workspace:^", + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@eslint/js": "catalog:devTools", + "@types/content-type": "catalog:devTools", + "@types/cors": "catalog:devTools", + "@types/cross-spawn": "catalog:devTools", + "@types/eventsource": "catalog:devTools", + "@types/express": "catalog:devTools", + "@types/express-serve-static-core": "catalog:devTools", + "@types/supertest": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", + "eslint": "catalog:devTools", + "eslint-config-prettier": "catalog:devTools", + "eslint-plugin-n": "catalog:devTools", + "prettier": "catalog:devTools", + "supertest": "catalog:devTools", + "tsdown": "catalog:devTools", + "tsx": "catalog:devTools", + "typescript": "catalog:devTools", + "typescript-eslint": "catalog:devTools", + "vitest": "catalog:devTools" + } +} diff --git a/packages/middleware/node/src/index.ts b/packages/middleware/node/src/index.ts new file mode 100644 index 000000000..2e0d3c995 --- /dev/null +++ b/packages/middleware/node/src/index.ts @@ -0,0 +1 @@ +export * from './streamableHttp.js'; diff --git a/packages/middleware/node/src/streamableHttp.ts b/packages/middleware/node/src/streamableHttp.ts new file mode 100644 index 000000000..2c107fdf5 --- /dev/null +++ b/packages/middleware/node/src/streamableHttp.ts @@ -0,0 +1,188 @@ +/** + * Node.js HTTP Streamable HTTP Server Transport + * + * This is a thin wrapper around `WebStandardStreamableHTTPServerTransport` that provides + * compatibility with Node.js HTTP server (IncomingMessage/ServerResponse). + * + * For web-standard environments (Cloudflare Workers, Deno, Bun), use `WebStandardStreamableHTTPServerTransport` directly. + */ + +import type { IncomingMessage, ServerResponse } from 'node:http'; + +import { getRequestListener } from '@hono/node-server'; +import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport } from '@modelcontextprotocol/core'; +import type { WebStandardStreamableHTTPServerTransportOptions } from '@modelcontextprotocol/server'; +import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; + +/** + * Configuration options for StreamableHTTPServerTransport + * + * This is an alias for WebStandardStreamableHTTPServerTransportOptions for backward compatibility. + */ +export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions; + +/** + * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. + * It supports both SSE streaming and direct HTTP responses. + * + * This is a wrapper around `WebStandardStreamableHTTPServerTransport` that provides Node.js HTTP compatibility. + * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs. + * + * Usage example: + * + * ```typescript + * // Stateful mode - server sets the session ID + * const statefulTransport = new StreamableHTTPServerTransport({ + * sessionIdGenerator: () => randomUUID(), + * }); + * + * // Stateless mode - explicitly set session ID to undefined + * const statelessTransport = new StreamableHTTPServerTransport({ + * sessionIdGenerator: undefined, + * }); + * + * // Using with pre-parsed request body + * app.post('/mcp', (req, res) => { + * transport.handleRequest(req, res, req.body); + * }); + * ``` + * + * In stateful mode: + * - Session ID is generated and included in response headers + * - Session ID is always included in initialization responses + * - Requests with invalid session IDs are rejected with 404 Not Found + * - Non-initialization requests without a session ID are rejected with 400 Bad Request + * - State is maintained in-memory (connections, message history) + * + * In stateless mode: + * - No Session ID is included in any responses + * - No session validation is performed + */ +export class NodeStreamableHTTPServerTransport implements Transport { + private _webStandardTransport: WebStandardStreamableHTTPServerTransport; + private _requestListener: ReturnType; + // Store auth and parsedBody per request for passing through to handleRequest + private _requestContext: WeakMap = new WeakMap(); + + constructor(options: StreamableHTTPServerTransportOptions = {}) { + this._webStandardTransport = new WebStandardStreamableHTTPServerTransport(options); + + // Create a request listener that wraps the web standard transport + // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming + this._requestListener = getRequestListener(async (webRequest: Request) => { + // Get context if available (set during handleRequest) + const context = this._requestContext.get(webRequest); + return this._webStandardTransport.handleRequest(webRequest, { + authInfo: context?.authInfo, + parsedBody: context?.parsedBody + }); + }); + } + + /** + * Gets the session ID for this transport instance. + */ + get sessionId(): string | undefined { + return this._webStandardTransport.sessionId; + } + + /** + * Sets callback for when the transport is closed. + */ + set onclose(handler: (() => void) | undefined) { + this._webStandardTransport.onclose = handler; + } + + get onclose(): (() => void) | undefined { + return this._webStandardTransport.onclose; + } + + /** + * Sets callback for transport errors. + */ + set onerror(handler: ((error: Error) => void) | undefined) { + this._webStandardTransport.onerror = handler; + } + + get onerror(): ((error: Error) => void) | undefined { + return this._webStandardTransport.onerror; + } + + /** + * Sets callback for incoming messages. + */ + set onmessage(handler: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined) { + this._webStandardTransport.onmessage = handler; + } + + get onmessage(): ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined { + return this._webStandardTransport.onmessage; + } + + /** + * Starts the transport. This is required by the Transport interface but is a no-op + * for the Streamable HTTP transport as connections are managed per-request. + */ + async start(): Promise { + return this._webStandardTransport.start(); + } + + /** + * Closes the transport and all active connections. + */ + async close(): Promise { + return this._webStandardTransport.close(); + } + + /** + * Sends a JSON-RPC message through the transport. + */ + async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise { + return this._webStandardTransport.send(message, options); + } + + /** + * Handles an incoming HTTP request, whether GET or POST. + * + * This method converts Node.js HTTP objects to Web Standard Request/Response + * and delegates to the underlying WebStandardStreamableHTTPServerTransport. + * + * @param req - Node.js IncomingMessage, optionally with auth property from middleware + * @param res - Node.js ServerResponse + * @param parsedBody - Optional pre-parsed body from body-parser middleware + */ + async handleRequest(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise { + // Store context for this request to pass through auth and parsedBody + // We need to intercept the request creation to attach this context + const authInfo = req.auth; + + // Create a custom handler that includes our context + const handler = getRequestListener(async (webRequest: Request) => { + return this._webStandardTransport.handleRequest(webRequest, { + authInfo, + parsedBody + }); + }); + + // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion + // including proper SSE streaming support + await handler(req, res); + } + + /** + * Close an SSE stream for a specific request, triggering client reconnection. + * Use this to implement polling behavior during long-running operations - + * client will reconnect after the retry interval specified in the priming event. + */ + closeSSEStream(requestId: RequestId): void { + this._webStandardTransport.closeSSEStream(requestId); + } + + /** + * Close the standalone GET SSE stream, triggering client reconnection. + * Use this to implement polling behavior for server-initiated notifications. + */ + closeStandaloneSSEStream(): void { + this._webStandardTransport.closeStandaloneSSEStream(); + } +} diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts new file mode 100644 index 000000000..ea6c09333 --- /dev/null +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -0,0 +1,2987 @@ +import { randomUUID } from 'node:crypto'; +import type { IncomingMessage, Server, ServerResponse } from 'node:http'; +import { createServer } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { createServer as netCreateServer } from 'node:net'; + +import type { + AuthInfo, + CallToolResult, + JSONRPCErrorResponse, + JSONRPCMessage, + JSONRPCResultResponse, + RequestId +} from '@modelcontextprotocol/core'; +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; + +import { NodeStreamableHTTPServerTransport } from '../src/streamableHttp.js'; +import { McpServer } from '@modelcontextprotocol/server'; +import type { EventId, EventStore, StreamId } from '@modelcontextprotocol/server'; +import { describe, expect, beforeEach, afterEach, it } from 'vitest'; + +async function getFreePort() { + return new Promise(res => { + const srv = netCreateServer(); + srv.listen(0, () => { + const address = srv.address()!; + if (typeof address === 'string') { + throw new Error('Unexpected address type: ' + typeof address); + } + const port = (address as AddressInfo).port; + srv.close(_err => res(port)); + }); + }); +} + +/** + * Test server configuration for NodeStreamableHTTPServerTransport tests + */ +interface TestServerConfig { + sessionIdGenerator: (() => string) | undefined; + enableJsonResponse?: boolean; + customRequestHandler?: (req: IncomingMessage, res: ServerResponse, parsedBody?: unknown) => Promise; + eventStore?: EventStore; + onsessioninitialized?: (sessionId: string) => void | Promise; + onsessionclosed?: (sessionId: string) => void | Promise; + retryInterval?: number; +} + +/** + * Helper to stop test server + */ +async function stopTestServer({ server, transport }: { server: Server; transport: NodeStreamableHTTPServerTransport }): Promise { + // First close the transport to ensure all SSE streams are closed + await transport.close(); + + // Close the server without waiting indefinitely + server.close(); +} + +/** + * Common test messages + */ +const TEST_MESSAGES = { + initialize: { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client', version: '1.0' }, + protocolVersion: '2025-11-25', + capabilities: {} + }, + id: 'init-1' + } as JSONRPCMessage, + + // Initialize message with an older protocol version for backward compatibility tests + initializeOldVersion: { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client', version: '1.0' }, + protocolVersion: '2025-06-18', + capabilities: {} + }, + id: 'init-1' + } as JSONRPCMessage, + + toolsList: { + jsonrpc: '2.0', + method: 'tools/list', + params: {}, + id: 'tools-1' + } as JSONRPCMessage +}; + +/** + * Helper to extract text from SSE response + * Note: Can only be called once per response stream. For multiple reads, + * get the reader manually and read multiple times. + */ +async function readSSEEvent(response: Response): Promise { + const reader = response.body?.getReader(); + const { value } = await reader!.read(); + return new TextDecoder().decode(value); +} + +/** + * Helper to send JSON-RPC request + */ +async function sendPostRequest( + baseUrl: URL, + message: JSONRPCMessage | JSONRPCMessage[], + sessionId?: string, + extraHeaders?: Record +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + ...extraHeaders + }; + + if (sessionId) { + headers['mcp-session-id'] = sessionId; + headers['mcp-protocol-version'] = '2025-11-25'; + } + + return fetch(baseUrl, { + method: 'POST', + headers, + body: JSON.stringify(message) + }); +} + +function expectErrorResponse( + data: unknown, + expectedCode: number, + expectedMessagePattern: RegExp, + options?: { expectData?: boolean } +): void { + expect(data).toMatchObject({ + jsonrpc: '2.0', + error: expect.objectContaining({ + code: expectedCode, + message: expect.stringMatching(expectedMessagePattern) + }) + }); + if (options?.expectData) { + expect((data as { error: { data?: string } }).error.data).toBeDefined(); + } +} +describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { + /** + * Helper to create and start test HTTP server with MCP setup + */ + async function createTestServer(config: TestServerConfig = { sessionIdGenerator: () => randomUUID() }): Promise<{ + server: Server; + transport: NodeStreamableHTTPServerTransport; + mcpServer: McpServer; + baseUrl: URL; + }> { + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + + mcpServer.tool( + 'greet', + 'A simple greeting tool', + { name: z.string().describe('Name to greet') }, + async ({ name }): Promise => { + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + } + ); + + const transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: config.sessionIdGenerator, + enableJsonResponse: config.enableJsonResponse ?? false, + eventStore: config.eventStore, + onsessioninitialized: config.onsessioninitialized, + onsessionclosed: config.onsessionclosed, + retryInterval: config.retryInterval + }); + + await mcpServer.connect(transport); + + const server = createServer(async (req, res) => { + try { + if (config.customRequestHandler) { + await config.customRequestHandler(req, res); + } else { + await transport.handleRequest(req, res); + } + } catch (error) { + console.error('Error handling request:', error); + if (!res.headersSent) res.writeHead(500).end(); + } + }); + + const baseUrl = await listenOnRandomPort(server); + + return { server, transport, mcpServer, baseUrl }; + } + + /** + * Helper to create and start authenticated test HTTP server with MCP setup + */ + async function createTestAuthServer(config: TestServerConfig = { sessionIdGenerator: () => randomUUID() }): Promise<{ + server: Server; + transport: NodeStreamableHTTPServerTransport; + mcpServer: McpServer; + baseUrl: URL; + }> { + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + + mcpServer.tool( + 'profile', + 'A user profile data tool', + { active: z.boolean().describe('Profile status') }, + async ({ active }, { authInfo }): Promise => { + return { content: [{ type: 'text', text: `${active ? 'Active' : 'Inactive'} profile from token: ${authInfo?.token}!` }] }; + } + ); + + const transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: config.sessionIdGenerator, + enableJsonResponse: config.enableJsonResponse ?? false, + eventStore: config.eventStore, + onsessioninitialized: config.onsessioninitialized, + onsessionclosed: config.onsessionclosed + }); + + await mcpServer.connect(transport); + + const server = createServer(async (req: IncomingMessage & { auth?: AuthInfo }, res) => { + try { + if (config.customRequestHandler) { + await config.customRequestHandler(req, res); + } else { + req.auth = { token: req.headers['authorization']?.split(' ')[1] } as AuthInfo; + await transport.handleRequest(req, res); + } + } catch (error) { + console.error('Error handling request:', error); + if (!res.headersSent) res.writeHead(500).end(); + } + }); + + const baseUrl = await listenOnRandomPort(server); + + return { server, transport, mcpServer, baseUrl }; + } + + const { z } = entry; + describe('NodeStreamableHTTPServerTransport', () => { + let server: Server; + let mcpServer: McpServer; + let transport: NodeStreamableHTTPServerTransport; + let baseUrl: URL; + let sessionId: string; + + beforeEach(async () => { + const result = await createTestServer(); + server = result.server; + transport = result.transport; + mcpServer = result.mcpServer; + baseUrl = result.baseUrl; + }); + + afterEach(async () => { + await stopTestServer({ server, transport }); + }); + + async function initializeServer(): Promise { + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + + expect(response.status).toBe(200); + const newSessionId = response.headers.get('mcp-session-id'); + expect(newSessionId).toBeDefined(); + return newSessionId as string; + } + + it('should initialize server and generate session ID', async () => { + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + expect(response.headers.get('mcp-session-id')).toBeDefined(); + }); + + it('should reject second initialization request', async () => { + // First initialize + const sessionId = await initializeServer(); + expect(sessionId).toBeDefined(); + + // Try second initialize + const secondInitMessage = { + ...TEST_MESSAGES.initialize, + id: 'second-init' + }; + + const response = await sendPostRequest(baseUrl, secondInitMessage); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32600, /Server already initialized/); + }); + + it('should reject batch initialize request', async () => { + const batchInitMessages: JSONRPCMessage[] = [ + TEST_MESSAGES.initialize, + { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client-2', version: '1.0' }, + protocolVersion: '2025-03-26' + }, + id: 'init-2' + } + ]; + + const response = await sendPostRequest(baseUrl, batchInitMessages); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32600, /Only one initialization request is allowed/); + }); + + it('should handle post requests via sse response correctly', async () => { + sessionId = await initializeServer(); + + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId); + + expect(response.status).toBe(200); + + // Read the SSE stream for the response + const text = await readSSEEvent(response); + + // Parse the SSE event + const eventLines = text.split('\n'); + const dataLine = eventLines.find(line => line.startsWith('data:')); + expect(dataLine).toBeDefined(); + + const eventData = JSON.parse(dataLine!.substring(5)); + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: expect.objectContaining({ + tools: expect.arrayContaining([ + expect.objectContaining({ + name: 'greet', + description: 'A simple greeting tool' + }) + ]) + }), + id: 'tools-1' + }); + }); + + it('should call a tool and return the result', async () => { + sessionId = await initializeServer(); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'greet', + arguments: { + name: 'Test User' + } + }, + id: 'call-1' + }; + + const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId); + expect(response.status).toBe(200); + + const text = await readSSEEvent(response); + const eventLines = text.split('\n'); + const dataLine = eventLines.find(line => line.startsWith('data:')); + expect(dataLine).toBeDefined(); + + const eventData = JSON.parse(dataLine!.substring(5)); + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: { + content: [ + { + type: 'text', + text: 'Hello, Test User!' + } + ] + }, + id: 'call-1' + }); + }); + + /*** + * Test: Tool With Request Info + */ + it('should pass request info to tool callback', async () => { + sessionId = await initializeServer(); + + mcpServer.tool( + 'test-request-info', + 'A simple test tool with request info', + { name: z.string().describe('Name to greet') }, + async ({ name }, { requestInfo }): Promise => { + return { + content: [ + { type: 'text', text: `Hello, ${name}!` }, + { type: 'text', text: `${JSON.stringify(requestInfo)}` } + ] + }; + } + ); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'test-request-info', + arguments: { + name: 'Test User' + } + }, + id: 'call-1' + }; + + const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId); + expect(response.status).toBe(200); + + const text = await readSSEEvent(response); + const eventLines = text.split('\n'); + const dataLine = eventLines.find(line => line.startsWith('data:')); + expect(dataLine).toBeDefined(); + + const eventData = JSON.parse(dataLine!.substring(5)); + + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: { + content: [ + { type: 'text', text: 'Hello, Test User!' }, + { type: 'text', text: expect.any(String) } + ] + }, + id: 'call-1' + }); + + const requestInfo = JSON.parse(eventData.result.content[1].text); + expect(requestInfo).toMatchObject({ + headers: { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + connection: 'keep-alive', + 'mcp-session-id': sessionId, + 'accept-language': '*', + 'user-agent': expect.any(String), + 'accept-encoding': expect.any(String), + 'content-length': expect.any(String) + } + }); + }); + + it('should reject requests without a valid session ID', async () => { + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList); + + expect(response.status).toBe(400); + const errorData = (await response.json()) as JSONRPCErrorResponse; + expectErrorResponse(errorData, -32000, /Bad Request/); + expect(errorData.id).toBeNull(); + }); + + it('should reject invalid session ID', async () => { + // First initialize to be in valid state + await initializeServer(); + + // Now try with invalid session ID + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, 'invalid-session-id'); + + expect(response.status).toBe(404); + const errorData = await response.json(); + expectErrorResponse(errorData, -32001, /Session not found/); + }); + + it('should establish standalone SSE stream and receive server-initiated messages', async () => { + // First initialize to get a session ID + sessionId = await initializeServer(); + + // Open a standalone SSE stream + const sseResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(sseResponse.status).toBe(200); + expect(sseResponse.headers.get('content-type')).toBe('text/event-stream'); + + // Send a notification (server-initiated message) that should appear on SSE stream + const notification: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'notifications/message', + params: { level: 'info', data: 'Test notification' } + }; + + // Send the notification via transport + await transport.send(notification); + + // Read from the stream and verify we got the notification + const text = await readSSEEvent(sseResponse); + + const eventLines = text.split('\n'); + const dataLine = eventLines.find(line => line.startsWith('data:')); + expect(dataLine).toBeDefined(); + + const eventData = JSON.parse(dataLine!.substring(5)); + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + method: 'notifications/message', + params: { level: 'info', data: 'Test notification' } + }); + }); + + it('should not close GET SSE stream after sending multiple server notifications', async () => { + sessionId = await initializeServer(); + + // Open a standalone SSE stream + const sseResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(sseResponse.status).toBe(200); + const reader = sseResponse.body?.getReader(); + + // Send multiple notifications + const notification1: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'notifications/message', + params: { level: 'info', data: 'First notification' } + }; + + // Just send one and verify it comes through - then the stream should stay open + await transport.send(notification1); + + const { value, done } = await reader!.read(); + const text = new TextDecoder().decode(value); + expect(text).toContain('First notification'); + expect(done).toBe(false); // Stream should still be open + }); + + it('should reject second SSE stream for the same session', async () => { + sessionId = await initializeServer(); + + // Open first SSE stream + const firstStream = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(firstStream.status).toBe(200); + + // Try to open a second SSE stream with the same session ID + const secondStream = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + + // Should be rejected + expect(secondStream.status).toBe(409); // Conflict + const errorData = await secondStream.json(); + expectErrorResponse(errorData, -32000, /Only one SSE stream is allowed per session/); + }); + + it('should reject GET requests without Accept: text/event-stream header', async () => { + sessionId = await initializeServer(); + + // Try GET without proper Accept header + const response = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(response.status).toBe(406); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Client must accept text\/event-stream/); + }); + + it('should reject POST requests without proper Accept header', async () => { + sessionId = await initializeServer(); + + // Try POST without Accept: text/event-stream + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', // Missing text/event-stream + 'mcp-session-id': sessionId + }, + body: JSON.stringify(TEST_MESSAGES.toolsList) + }); + + expect(response.status).toBe(406); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Client must accept both application\/json and text\/event-stream/); + }); + + it('should reject unsupported Content-Type', async () => { + sessionId = await initializeServer(); + + // Try POST with text/plain Content-Type + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId + }, + body: 'This is plain text' + }); + + expect(response.status).toBe(415); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Content-Type must be application\/json/); + }); + + it('should handle JSON-RPC batch notification messages with 202 response', async () => { + sessionId = await initializeServer(); + + // Send batch of notifications (no IDs) + const batchNotifications: JSONRPCMessage[] = [ + { jsonrpc: '2.0', method: 'someNotification1', params: {} }, + { jsonrpc: '2.0', method: 'someNotification2', params: {} } + ]; + const response = await sendPostRequest(baseUrl, batchNotifications, sessionId); + + expect(response.status).toBe(202); + expect(response.headers.get('content-type')).toBe('application/json'); + }); + + it('should handle batch request messages with SSE stream for responses', async () => { + sessionId = await initializeServer(); + + // Send batch of requests + const batchRequests: JSONRPCMessage[] = [ + { jsonrpc: '2.0', method: 'tools/list', params: {}, id: 'req-1' }, + { jsonrpc: '2.0', method: 'tools/call', params: { name: 'greet', arguments: { name: 'BatchUser' } }, id: 'req-2' } + ]; + const response = await sendPostRequest(baseUrl, batchRequests, sessionId); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + + const reader = response.body?.getReader(); + + // The responses may come in any order or together in one chunk + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // Check that both responses were sent on the same stream + expect(text).toContain('"id":"req-1"'); + expect(text).toContain('"tools"'); // tools/list result + expect(text).toContain('"id":"req-2"'); + expect(text).toContain('Hello, BatchUser'); // tools/call result + }); + + it('should properly handle invalid JSON data', async () => { + sessionId = await initializeServer(); + + // Send invalid JSON + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId + }, + body: 'This is not valid JSON' + }); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32700, /Parse error/); + }); + + it('should include error data in parse error response for unexpected errors', async () => { + sessionId = await initializeServer(); + + // We can't easily trigger the catch-all error handler, but we can verify + // that the JSON parse error includes useful information + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId + }, + body: '{ invalid json }' + }); + + expect(response.status).toBe(400); + const errorData = (await response.json()) as JSONRPCErrorResponse; + expectErrorResponse(errorData, -32700, /Parse error/); + // The error message should contain details about what went wrong + expect(errorData.error.message).toContain('Invalid JSON'); + }); + + it('should return 400 error for invalid JSON-RPC messages', async () => { + sessionId = await initializeServer(); + + // Invalid JSON-RPC (missing required jsonrpc version) + const invalidMessage = { method: 'tools/list', params: {}, id: 1 }; // missing jsonrpc version + const response = await sendPostRequest(baseUrl, invalidMessage as JSONRPCMessage, sessionId); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expect(errorData).toMatchObject({ + jsonrpc: '2.0', + error: expect.anything() + }); + }); + + it('should reject requests to uninitialized server', async () => { + // Create a new HTTP server and transport without initializing + const { server: uninitializedServer, transport: uninitializedTransport, baseUrl: uninitializedUrl } = await createTestServer(); + // Transport not used in test but needed for cleanup + + // No initialization, just send a request directly + const uninitializedMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/list', + params: {}, + id: 'uninitialized-test' + }; + + // Send a request to uninitialized server + const response = await sendPostRequest(uninitializedUrl, uninitializedMessage, 'any-session-id'); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Server not initialized/); + + // Cleanup + await stopTestServer({ server: uninitializedServer, transport: uninitializedTransport }); + }); + + it('should send response messages to the connection that sent the request', async () => { + sessionId = await initializeServer(); + + const message1: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/list', + params: {}, + id: 'req-1' + }; + + const message2: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'greet', + arguments: { name: 'Connection2' } + }, + id: 'req-2' + }; + + // Make two concurrent fetch connections for different requests + const req1 = sendPostRequest(baseUrl, message1, sessionId); + const req2 = sendPostRequest(baseUrl, message2, sessionId); + + // Get both responses + const [response1, response2] = await Promise.all([req1, req2]); + const reader1 = response1.body?.getReader(); + const reader2 = response2.body?.getReader(); + + // Read responses from each stream (requires each receives its specific response) + const { value: value1 } = await reader1!.read(); + const text1 = new TextDecoder().decode(value1); + expect(text1).toContain('"id":"req-1"'); + expect(text1).toContain('"tools"'); // tools/list result + + const { value: value2 } = await reader2!.read(); + const text2 = new TextDecoder().decode(value2); + expect(text2).toContain('"id":"req-2"'); + expect(text2).toContain('Hello, Connection2'); // tools/call result + }); + + it('should keep stream open after sending server notifications', async () => { + sessionId = await initializeServer(); + + // Open a standalone SSE stream + const sseResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + + // Send several server-initiated notifications + await transport.send({ + jsonrpc: '2.0', + method: 'notifications/message', + params: { level: 'info', data: 'First notification' } + }); + + await transport.send({ + jsonrpc: '2.0', + method: 'notifications/message', + params: { level: 'info', data: 'Second notification' } + }); + + // Stream should still be open - it should not close after sending notifications + expect(sseResponse.bodyUsed).toBe(false); + }); + + // The current implementation will close the entire transport for DELETE + // Creating a temporary transport/server where we don't care if it gets closed + it('should properly handle DELETE requests and close session', async () => { + // Setup a temporary server for this test + const tempResult = await createTestServer(); + const tempServer = tempResult.server; + const tempUrl = tempResult.baseUrl; + + // Initialize to get a session ID + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + const tempSessionId = initResponse.headers.get('mcp-session-id'); + + // Now DELETE the session + const deleteResponse = await fetch(tempUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': tempSessionId || '', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(deleteResponse.status).toBe(200); + + // Clean up - don't wait indefinitely for server close + tempServer.close(); + }); + + it('should reject DELETE requests with invalid session ID', async () => { + // Initialize the server first to activate it + sessionId = await initializeServer(); + + // Try to delete with invalid session ID + const response = await fetch(baseUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': 'invalid-session-id', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(response.status).toBe(404); + const errorData = await response.json(); + expectErrorResponse(errorData, -32001, /Session not found/); + }); + + describe('protocol version header validation', () => { + it('should accept requests with matching protocol version', async () => { + sessionId = await initializeServer(); + + // Send request with matching protocol version + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId); + + expect(response.status).toBe(200); + }); + + it('should accept requests without protocol version header', async () => { + sessionId = await initializeServer(); + + // Send request without protocol version header + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId + // No mcp-protocol-version header + }, + body: JSON.stringify(TEST_MESSAGES.toolsList) + }); + + expect(response.status).toBe(200); + }); + + it('should reject requests with unsupported protocol version', async () => { + sessionId = await initializeServer(); + + // Send request with unsupported protocol version + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '1999-01-01' // Unsupported version + }, + body: JSON.stringify(TEST_MESSAGES.toolsList) + }); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version: .+ \(supported versions: .+\)/); + }); + + it('should accept when protocol version differs from negotiated version', async () => { + sessionId = await initializeServer(); + + // Send request with different but supported protocol version + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2024-11-05' // Different but supported version + }, + body: JSON.stringify(TEST_MESSAGES.toolsList) + }); + + // Request should still succeed + expect(response.status).toBe(200); + }); + + it('should reject unsupported protocol version on GET requests', async () => { + sessionId = await initializeServer(); + + // GET request with unsupported protocol version + const response = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '1999-01-01' // Unsupported version + } + }); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + }); + + it('should reject unsupported protocol version on DELETE requests', async () => { + sessionId = await initializeServer(); + + // DELETE request with unsupported protocol version + const response = await fetch(baseUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '1999-01-01' // Unsupported version + } + }); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + }); + }); + }); + + describe('NodeStreamableHTTPServerTransport with AuthInfo', () => { + let server: Server; + let transport: NodeStreamableHTTPServerTransport; + let baseUrl: URL; + let sessionId: string; + + beforeEach(async () => { + const result = await createTestAuthServer(); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + }); + + afterEach(async () => { + await stopTestServer({ server, transport }); + }); + + async function initializeServer(): Promise { + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + + expect(response.status).toBe(200); + const newSessionId = response.headers.get('mcp-session-id'); + expect(newSessionId).toBeDefined(); + return newSessionId as string; + } + + it('should call a tool with authInfo', async () => { + sessionId = await initializeServer(); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'profile', + arguments: { active: true } + }, + id: 'call-1' + }; + + const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId, { authorization: 'Bearer test-token' }); + expect(response.status).toBe(200); + + const text = await readSSEEvent(response); + const eventLines = text.split('\n'); + const dataLine = eventLines.find(line => line.startsWith('data:')); + expect(dataLine).toBeDefined(); + + const eventData = JSON.parse(dataLine!.substring(5)); + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: { + content: [ + { + type: 'text', + text: 'Active profile from token: test-token!' + } + ] + }, + id: 'call-1' + }); + }); + + it('should calls tool without authInfo when it is optional', async () => { + sessionId = await initializeServer(); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'profile', + arguments: { active: false } + }, + id: 'call-1' + }; + + const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId); + expect(response.status).toBe(200); + + const text = await readSSEEvent(response); + const eventLines = text.split('\n'); + const dataLine = eventLines.find(line => line.startsWith('data:')); + expect(dataLine).toBeDefined(); + + const eventData = JSON.parse(dataLine!.substring(5)); + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: { + content: [ + { + type: 'text', + text: 'Inactive profile from token: undefined!' + } + ] + }, + id: 'call-1' + }); + }); + }); + + // Test JSON Response Mode + describe('NodeStreamableHTTPServerTransport with JSON Response Mode', () => { + let server: Server; + let transport: NodeStreamableHTTPServerTransport; + let baseUrl: URL; + let sessionId: string; + + beforeEach(async () => { + const result = await createTestServer({ sessionIdGenerator: () => randomUUID(), enableJsonResponse: true }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + // Initialize and get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + + sessionId = initResponse.headers.get('mcp-session-id') as string; + }); + + afterEach(async () => { + await stopTestServer({ server, transport }); + }); + + it('should return JSON response for a single request', async () => { + const toolsListMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/list', + params: {}, + id: 'json-req-1' + }; + + const response = await sendPostRequest(baseUrl, toolsListMessage, sessionId); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); + + const result = await response.json(); + expect(result).toMatchObject({ + jsonrpc: '2.0', + result: expect.objectContaining({ + tools: expect.arrayContaining([expect.objectContaining({ name: 'greet' })]) + }), + id: 'json-req-1' + }); + }); + + it('should return JSON response for batch requests', async () => { + const batchMessages: JSONRPCMessage[] = [ + { jsonrpc: '2.0', method: 'tools/list', params: {}, id: 'batch-1' }, + { jsonrpc: '2.0', method: 'tools/call', params: { name: 'greet', arguments: { name: 'JSON' } }, id: 'batch-2' } + ]; + + const response = await sendPostRequest(baseUrl, batchMessages, sessionId); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); + + const results = (await response.json()) as JSONRPCResultResponse[]; + expect(Array.isArray(results)).toBe(true); + expect(results).toHaveLength(2); + + // Batch responses can come in any order + const listResponse = results.find((r: { id?: RequestId }) => r.id === 'batch-1'); + const callResponse = results.find((r: { id?: RequestId }) => r.id === 'batch-2'); + + expect(listResponse).toEqual( + expect.objectContaining({ + jsonrpc: '2.0', + id: 'batch-1', + result: expect.objectContaining({ + tools: expect.arrayContaining([expect.objectContaining({ name: 'greet' })]) + }) + }) + ); + + expect(callResponse).toEqual( + expect.objectContaining({ + jsonrpc: '2.0', + id: 'batch-2', + result: expect.objectContaining({ + content: expect.arrayContaining([expect.objectContaining({ type: 'text', text: 'Hello, JSON!' })]) + }) + }) + ); + }); + }); + + // Test pre-parsed body handling + describe('NodeStreamableHTTPServerTransport with pre-parsed body', () => { + let server: Server; + let transport: NodeStreamableHTTPServerTransport; + let baseUrl: URL; + let sessionId: string; + let parsedBody: unknown = null; + + beforeEach(async () => { + const result = await createTestServer({ + customRequestHandler: async (req, res) => { + try { + if (parsedBody !== null) { + await transport.handleRequest(req, res, parsedBody); + parsedBody = null; // Reset after use + } else { + await transport.handleRequest(req, res); + } + } catch (error) { + console.error('Error handling request:', error); + if (!res.headersSent) res.writeHead(500).end(); + } + }, + sessionIdGenerator: () => randomUUID() + }); + + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + // Initialize and get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + }); + + afterEach(async () => { + await stopTestServer({ server, transport }); + }); + + it('should accept pre-parsed request body', async () => { + // Set up the pre-parsed body + parsedBody = { + jsonrpc: '2.0', + method: 'tools/list', + params: {}, + id: 'preparsed-1' + }; + + // Send an empty body since we'll use pre-parsed body + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId + }, + // Empty body - we're testing pre-parsed body + body: '' + }); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + + const reader = response.body?.getReader(); + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // Verify the response used the pre-parsed body + expect(text).toContain('"id":"preparsed-1"'); + expect(text).toContain('"tools"'); + }); + + it('should handle pre-parsed batch messages', async () => { + parsedBody = [ + { jsonrpc: '2.0', method: 'tools/list', params: {}, id: 'batch-1' }, + { jsonrpc: '2.0', method: 'tools/call', params: { name: 'greet', arguments: { name: 'PreParsed' } }, id: 'batch-2' } + ]; + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId + }, + body: '' // Empty as we're using pre-parsed + }); + + expect(response.status).toBe(200); + + const reader = response.body?.getReader(); + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + expect(text).toContain('"id":"batch-1"'); + expect(text).toContain('"tools"'); + }); + + it('should prefer pre-parsed body over request body', async () => { + // Set pre-parsed to tools/list + parsedBody = { + jsonrpc: '2.0', + method: 'tools/list', + params: {}, + id: 'preparsed-wins' + }; + + // Send actual body with tools/call - should be ignored + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'tools/call', + params: { name: 'greet', arguments: { name: 'Ignored' } }, + id: 'ignored-id' + }) + }); + + expect(response.status).toBe(200); + + const reader = response.body?.getReader(); + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // Should have processed the pre-parsed body + expect(text).toContain('"id":"preparsed-wins"'); + expect(text).toContain('"tools"'); + expect(text).not.toContain('"ignored-id"'); + }); + }); + + // Test resumability support + describe('NodeStreamableHTTPServerTransport with resumability', () => { + let server: Server; + let transport: NodeStreamableHTTPServerTransport; + let baseUrl: URL; + let sessionId: string; + let mcpServer: McpServer; + const storedEvents: Map = new Map(); + + // Simple implementation of EventStore + const eventStore: EventStore = { + async storeEvent(streamId: string, message: JSONRPCMessage): Promise { + const eventId = `${streamId}_${randomUUID()}`; + storedEvents.set(eventId, { eventId, message }); + return eventId; + }, + + async replayEventsAfter( + lastEventId: EventId, + { + send + }: { + send: (eventId: EventId, message: JSONRPCMessage) => Promise; + } + ): Promise { + const streamId = lastEventId.split('_')[0]!; + // Extract stream ID from the event ID + // For test simplicity, just return all events with matching streamId that aren't the lastEventId + for (const [eventId, { message }] of storedEvents.entries()) { + if (eventId.startsWith(streamId) && eventId !== lastEventId) { + await send(eventId, message); + } + } + return streamId; + } + }; + + beforeEach(async () => { + storedEvents.clear(); + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore + }); + + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Initialize the server + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + }); + + afterEach(async () => { + await stopTestServer({ server, transport }); + storedEvents.clear(); + }); + + it('should store and include event IDs in server SSE messages', async () => { + // Open a standalone SSE stream + const sseResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(sseResponse.status).toBe(200); + expect(sseResponse.headers.get('content-type')).toBe('text/event-stream'); + + // Send a notification that should be stored with an event ID + const notification: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'notifications/message', + params: { level: 'info', data: 'Test notification with event ID' } + }; + + // Send the notification via transport + await transport.send(notification); + + // Read from the stream and verify we got the notification with an event ID + const reader = sseResponse.body?.getReader(); + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // The response should contain an event ID + expect(text).toContain('id: '); + expect(text).toContain('"method":"notifications/message"'); + + // Extract the event ID + const idMatch = text.match(/id: ([^\n]+)/); + expect(idMatch).toBeTruthy(); + + // Verify the event was stored + const eventId = idMatch![1]!; + expect(storedEvents.has(eventId)).toBe(true); + const storedEvent = storedEvents.get(eventId); + expect(eventId.startsWith('_GET_stream')).toBe(true); + expect(storedEvent?.message).toMatchObject(notification); + }); + + it('should store and replay MCP server tool notifications', async () => { + // Establish a standalone SSE stream + const sseResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + expect(sseResponse.status).toBe(200); + + // Send a server notification through the MCP server + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'First notification from MCP server' }); + + // Read the notification from the SSE stream + const reader = sseResponse.body?.getReader(); + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // Verify the notification was sent with an event ID + expect(text).toContain('id: '); + expect(text).toContain('First notification from MCP server'); + + // Extract the event ID + const idMatch = text.match(/id: ([^\n]+)/); + expect(idMatch).toBeTruthy(); + const firstEventId = idMatch![1]!; + + // Send a second notification + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Second notification from MCP server' }); + + // Close the first SSE stream to simulate a disconnect + await reader!.cancel(); + + // Reconnect with the Last-Event-ID to get missed messages + const reconnectResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25', + 'last-event-id': firstEventId + } + }); + + expect(reconnectResponse.status).toBe(200); + + // Read the replayed notification + const reconnectReader = reconnectResponse.body?.getReader(); + const reconnectData = await reconnectReader!.read(); + const reconnectText = new TextDecoder().decode(reconnectData.value); + + // Verify we received the second notification that was sent after our stored eventId + expect(reconnectText).toContain('Second notification from MCP server'); + expect(reconnectText).toContain('id: '); + }); + + it('should store and replay multiple notifications sent while client is disconnected', async () => { + // Establish a standalone SSE stream + const sseResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + expect(sseResponse.status).toBe(200); + + const reader = sseResponse.body?.getReader(); + + // Send a notification to get an event ID + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Initial notification' }); + + // Read the notification from the SSE stream + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // Extract the event ID + const idMatch = text.match(/id: ([^\n]+)/); + expect(idMatch).toBeTruthy(); + const lastEventId = idMatch![1]!; + + // Close the SSE stream to simulate a disconnect + await reader!.cancel(); + + // Send MULTIPLE notifications while the client is disconnected + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Missed notification 1' }); + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Missed notification 2' }); + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Missed notification 3' }); + + // Reconnect with the Last-Event-ID to get all missed messages + const reconnectResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25', + 'last-event-id': lastEventId + } + }); + + expect(reconnectResponse.status).toBe(200); + + // Read replayed notifications with a timeout + const reconnectReader = reconnectResponse.body?.getReader(); + let allText = ''; + + // Read chunks until we have all 3 notifications or timeout + const readWithTimeout = async () => { + const timeout = setTimeout(() => reconnectReader!.cancel(), 2000); + try { + while (!allText.includes('Missed notification 3')) { + const { value, done } = await reconnectReader!.read(); + if (done) break; + allText += new TextDecoder().decode(value); + } + } finally { + clearTimeout(timeout); + } + }; + await readWithTimeout(); + + // Verify we received ALL notifications that were sent while disconnected + expect(allText).toContain('Missed notification 1'); + expect(allText).toContain('Missed notification 2'); + expect(allText).toContain('Missed notification 3'); + }); + }); + + // Test stateless mode + describe('NodeStreamableHTTPServerTransport in stateless mode', () => { + let server: Server; + let transport: NodeStreamableHTTPServerTransport; + let baseUrl: URL; + + beforeEach(async () => { + const result = await createTestServer({ sessionIdGenerator: undefined }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + }); + + afterEach(async () => { + await stopTestServer({ server, transport }); + }); + + it('should operate without session ID validation', async () => { + // Initialize the server first + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + + expect(initResponse.status).toBe(200); + // Should NOT have session ID header in stateless mode + expect(initResponse.headers.get('mcp-session-id')).toBeNull(); + + // Try request without session ID - should work in stateless mode + const toolsResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList); + + expect(toolsResponse.status).toBe(200); + }); + + it('should handle POST requests with various session IDs in stateless mode', async () => { + await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + + // Try with a random session ID - should be accepted + const response1 = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': 'random-id-1' + }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', params: {}, id: 't1' }) + }); + expect(response1.status).toBe(200); + + // Try with another random session ID - should also be accepted + const response2 = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': 'different-id-2' + }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', params: {}, id: 't2' }) + }); + expect(response2.status).toBe(200); + }); + + it('should reject second SSE stream even in stateless mode', async () => { + // Despite no session ID requirement, the transport still only allows + // one standalone SSE stream at a time + + // Initialize the server first + await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + + // Open first SSE stream + const stream1 = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-protocol-version': '2025-11-25' + } + }); + expect(stream1.status).toBe(200); + + // Open second SSE stream - should still be rejected, stateless mode still only allows one + const stream2 = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-protocol-version': '2025-11-25' + } + }); + expect(stream2.status).toBe(409); // Conflict - only one stream allowed + }); + }); + + // Test SSE priming events for POST streams + describe('NodeStreamableHTTPServerTransport POST SSE priming events', () => { + let server: Server; + let transport: NodeStreamableHTTPServerTransport; + let baseUrl: URL; + let sessionId: string; + let mcpServer: McpServer; + + // Simple eventStore for priming event tests + const createEventStore = (): EventStore => { + const storedEvents = new Map(); + return { + async storeEvent(streamId: string, message: JSONRPCMessage): Promise { + const eventId = `${streamId}::${Date.now()}_${randomUUID()}`; + storedEvents.set(eventId, { eventId, message, streamId }); + return eventId; + }, + async getStreamIdForEventId(eventId: string): Promise { + const event = storedEvents.get(eventId); + return event?.streamId; + }, + async replayEventsAfter( + lastEventId: EventId, + { send }: { send: (eventId: EventId, message: JSONRPCMessage) => Promise } + ): Promise { + const event = storedEvents.get(lastEventId); + const streamId = event?.streamId || lastEventId.split('::')[0]!; + const eventsToReplay: Array<[string, { message: JSONRPCMessage }]> = []; + for (const [eventId, data] of storedEvents.entries()) { + if (data.streamId === streamId && eventId > lastEventId) { + eventsToReplay.push([eventId, data]); + } + } + eventsToReplay.sort(([a], [b]) => a.localeCompare(b)); + for (const [eventId, { message }] of eventsToReplay) { + if (Object.keys(message).length > 0) { + await send(eventId, message); + } + } + return streamId; + } + }; + }; + + afterEach(async () => { + if (server && transport) { + await stopTestServer({ server, transport }); + } + }); + + it('should send priming event with retry field on POST SSE stream', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 5000 + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Initialize to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Send a tool call request + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 100, + method: 'tools/call', + params: { name: 'greet', arguments: { name: 'Test' } } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(toolCallRequest) + }); + + expect(postResponse.status).toBe(200); + expect(postResponse.headers.get('content-type')).toBe('text/event-stream'); + + // Read the priming event + const reader = postResponse.body?.getReader(); + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // Verify priming event has id and retry field + expect(text).toContain('id: '); + expect(text).toContain('retry: 5000'); + expect(text).toContain('data: '); + }); + + it('should NOT send priming event for old protocol versions (backwards compatibility)', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 5000 + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Initialize with OLD protocol version to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initializeOldVersion); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Send a tool call request with the same OLD protocol version + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 100, + method: 'tools/call', + params: { name: 'greet', arguments: { name: 'Test' } } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-06-18' + }, + body: JSON.stringify(toolCallRequest) + }); + + expect(postResponse.status).toBe(200); + expect(postResponse.headers.get('content-type')).toBe('text/event-stream'); + + // Read the first chunk - should be the actual response, not a priming event + const reader = postResponse.body?.getReader(); + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // Should NOT contain a priming event (empty data line before the response) + // The first message should be the actual tool result + expect(text).toContain('event: message'); + expect(text).toContain('"result"'); + // Should NOT have a separate priming event line with empty data + expect(text).not.toMatch(/^id:.*\ndata:\s*\n\n/); + }); + + it('should send priming event without retry field when retryInterval is not configured', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore() + // No retryInterval + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Initialize to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Send a tool call request + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 100, + method: 'tools/call', + params: { name: 'greet', arguments: { name: 'Test' } } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(toolCallRequest) + }); + + expect(postResponse.status).toBe(200); + + // Read the priming event + const reader = postResponse.body?.getReader(); + const { value } = await reader!.read(); + const text = new TextDecoder().decode(value); + + // Priming event should have id field but NOT retry field + expect(text).toContain('id: '); + expect(text).toContain('data: '); + expect(text).not.toContain('retry:'); + }); + + it('should close POST SSE stream when extra.closeSSEStream is called', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 1000 + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Track when stream close is called and tool completes + let streamCloseCalled = false; + let toolResolve: () => void; + const toolCompletePromise = new Promise(resolve => { + toolResolve = resolve; + }); + + // Register a tool that closes its own SSE stream via extra callback + mcpServer.tool('close-stream-tool', 'Closes its own stream', {}, async (_args, extra) => { + // Close the SSE stream for this request + extra.closeSSEStream?.(); + streamCloseCalled = true; + + // Wait before returning so we can observe the stream closure + await toolCompletePromise; + return { content: [{ type: 'text', text: 'Done' }] }; + }); + + // Initialize to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Send a tool call request + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 100, + method: 'tools/call', + params: { name: 'close-stream-tool', arguments: {} } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(toolCallRequest) + }); + + expect(postResponse.status).toBe(200); + + const reader = postResponse.body?.getReader(); + + // Read the priming event + await reader!.read(); + + // Wait a moment for the tool to call closeSSEStream + await new Promise(resolve => setTimeout(resolve, 100)); + expect(streamCloseCalled).toBe(true); + + // Stream should now be closed + const { done } = await reader!.read(); + expect(done).toBe(true); + + // Clean up - resolve the tool promise + toolResolve!(); + }); + + it('should provide closeSSEStream callback in extra when eventStore is configured', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 1000 + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Track whether closeSSEStream callback was provided + let receivedCloseSSEStream: (() => void) | undefined; + + // Register a tool that captures the extra.closeSSEStream callback + mcpServer.tool('test-callback-tool', 'Test tool', {}, async (_args, extra) => { + receivedCloseSSEStream = extra.closeSSEStream; + return { content: [{ type: 'text', text: 'Done' }] }; + }); + + // Initialize to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Call the tool + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 200, + method: 'tools/call', + params: { name: 'test-callback-tool', arguments: {} } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(toolCallRequest) + }); + + expect(postResponse.status).toBe(200); + + // Read all events to completion + const reader = postResponse.body?.getReader(); + while (true) { + const { done } = await reader!.read(); + if (done) break; + } + + // Verify closeSSEStream callback was provided + expect(receivedCloseSSEStream).toBeDefined(); + expect(typeof receivedCloseSSEStream).toBe('function'); + }); + + it('should NOT provide closeSSEStream callback for old protocol versions (backwards compatibility)', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 1000 + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Track whether closeSSEStream callback was provided + let receivedCloseSSEStream: (() => void) | undefined; + let receivedCloseStandaloneSSEStream: (() => void) | undefined; + + // Register a tool that captures the extra.closeSSEStream callback + mcpServer.tool('test-old-version-tool', 'Test tool', {}, async (_args, extra) => { + receivedCloseSSEStream = extra.closeSSEStream; + receivedCloseStandaloneSSEStream = extra.closeStandaloneSSEStream; + return { content: [{ type: 'text', text: 'Done' }] }; + }); + + // Initialize with OLD protocol version to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initializeOldVersion); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Call the tool with the same OLD protocol version + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 200, + method: 'tools/call', + params: { name: 'test-old-version-tool', arguments: {} } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-06-18' + }, + body: JSON.stringify(toolCallRequest) + }); + + expect(postResponse.status).toBe(200); + + // Read all events to completion + const reader = postResponse.body?.getReader(); + while (true) { + const { done } = await reader!.read(); + if (done) break; + } + + // Verify closeSSEStream callbacks were NOT provided for old protocol version + // even though eventStore is configured + expect(receivedCloseSSEStream).toBeUndefined(); + expect(receivedCloseStandaloneSSEStream).toBeUndefined(); + }); + + it('should NOT provide closeSSEStream callback when eventStore is NOT configured', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID() + // No eventStore + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Track whether closeSSEStream callback was provided + let receivedCloseSSEStream: (() => void) | undefined; + + // Register a tool that captures the extra.closeSSEStream callback + mcpServer.tool('test-no-callback-tool', 'Test tool', {}, async (_args, extra) => { + receivedCloseSSEStream = extra.closeSSEStream; + return { content: [{ type: 'text', text: 'Done' }] }; + }); + + // Initialize to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Call the tool + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 201, + method: 'tools/call', + params: { name: 'test-no-callback-tool', arguments: {} } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(toolCallRequest) + }); + + expect(postResponse.status).toBe(200); + + // Read all events to completion + const reader = postResponse.body?.getReader(); + while (true) { + const { done } = await reader!.read(); + if (done) break; + } + + // Verify closeSSEStream callback was NOT provided + expect(receivedCloseSSEStream).toBeUndefined(); + }); + + it('should provide closeStandaloneSSEStream callback in extra when eventStore is configured', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 1000 + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Track whether closeStandaloneSSEStream callback was provided + let receivedCloseStandaloneSSEStream: (() => void) | undefined; + + // Register a tool that captures the extra.closeStandaloneSSEStream callback + mcpServer.tool('test-standalone-callback-tool', 'Test tool', {}, async (_args, extra) => { + receivedCloseStandaloneSSEStream = extra.closeStandaloneSSEStream; + return { content: [{ type: 'text', text: 'Done' }] }; + }); + + // Initialize to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Call the tool + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 203, + method: 'tools/call', + params: { name: 'test-standalone-callback-tool', arguments: {} } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(toolCallRequest) + }); + + expect(postResponse.status).toBe(200); + + // Read all events to completion + const reader = postResponse.body?.getReader(); + while (true) { + const { done } = await reader!.read(); + if (done) break; + } + + // Verify closeStandaloneSSEStream callback was provided + expect(receivedCloseStandaloneSSEStream).toBeDefined(); + expect(typeof receivedCloseStandaloneSSEStream).toBe('function'); + }); + + it('should close standalone GET SSE stream when extra.closeStandaloneSSEStream is called', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 1000 + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Register a tool that closes the standalone SSE stream via extra callback + mcpServer.tool('close-standalone-stream-tool', 'Closes standalone stream', {}, async (_args, extra) => { + extra.closeStandaloneSSEStream?.(); + return { content: [{ type: 'text', text: 'Stream closed' }] }; + }); + + // Initialize to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Open a standalone GET SSE stream + const sseResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + expect(sseResponse.status).toBe(200); + + const getReader = sseResponse.body?.getReader(); + + // Send a notification to confirm GET stream is established + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Stream established' }); + + // Read the notification to confirm stream is working + const { value } = await getReader!.read(); + const text = new TextDecoder().decode(value); + expect(text).toContain('id: '); + expect(text).toContain('Stream established'); + + // Call the tool that closes the standalone SSE stream + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 300, + method: 'tools/call', + params: { name: 'close-standalone-stream-tool', arguments: {} } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(toolCallRequest) + }); + expect(postResponse.status).toBe(200); + + // Read the POST response to completion + const postReader = postResponse.body?.getReader(); + while (true) { + const { done } = await postReader!.read(); + if (done) break; + } + + // GET stream should now be closed - use a race with timeout to avoid hanging + const readPromise = getReader!.read(); + const timeoutPromise = new Promise<{ done: boolean; value: undefined }>((_, reject) => + setTimeout(() => reject(new Error('Stream did not close in time')), 1000) + ); + + const { done } = await Promise.race([readPromise, timeoutPromise]); + expect(done).toBe(true); + }); + + it('should allow client to reconnect after standalone SSE stream is closed via extra.closeStandaloneSSEStream', async () => { + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 1000 + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + mcpServer = result.mcpServer; + + // Register a tool that closes the standalone SSE stream + mcpServer.tool('close-standalone-for-reconnect', 'Closes standalone stream', {}, async (_args, extra) => { + extra.closeStandaloneSSEStream?.(); + return { content: [{ type: 'text', text: 'Stream closed' }] }; + }); + + // Initialize to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + sessionId = initResponse.headers.get('mcp-session-id') as string; + expect(sessionId).toBeDefined(); + + // Open a standalone GET SSE stream + const sseResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + } + }); + expect(sseResponse.status).toBe(200); + + const getReader = sseResponse.body?.getReader(); + + // Send a notification to get an event ID + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Initial message' }); + + // Read the notification to get the event ID + const { value } = await getReader!.read(); + const text = new TextDecoder().decode(value); + const idMatch = text.match(/id: ([^\n]+)/); + expect(idMatch).toBeTruthy(); + const lastEventId = idMatch![1]!; + + // Call the tool to close the standalone SSE stream + const toolCallRequest: JSONRPCMessage = { + jsonrpc: '2.0', + id: 301, + method: 'tools/call', + params: { name: 'close-standalone-for-reconnect', arguments: {} } + }; + + const postResponse = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(toolCallRequest) + }); + expect(postResponse.status).toBe(200); + + // Read the POST response to completion + const postReader = postResponse.body?.getReader(); + while (true) { + const { done } = await postReader!.read(); + if (done) break; + } + + // Wait for GET stream to close - use a race with timeout + const readPromise = getReader!.read(); + const timeoutPromise = new Promise<{ done: boolean; value: undefined }>((_, reject) => + setTimeout(() => reject(new Error('Stream did not close in time')), 1000) + ); + const { done } = await Promise.race([readPromise, timeoutPromise]); + expect(done).toBe(true); + + // Send a notification while client is disconnected + await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Missed while disconnected' }); + + // Client reconnects with Last-Event-ID + const reconnectResponse = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-11-25', + 'last-event-id': lastEventId + } + }); + expect(reconnectResponse.status).toBe(200); + + // Read the replayed notification + const reconnectReader = reconnectResponse.body?.getReader(); + let allText = ''; + const readWithTimeout = async () => { + const timeout = setTimeout(() => reconnectReader!.cancel(), 5000); + try { + while (!allText.includes('Missed while disconnected')) { + const { value, done } = await reconnectReader!.read(); + if (done) break; + allText += new TextDecoder().decode(value); + } + } finally { + clearTimeout(timeout); + } + }; + await readWithTimeout(); + + // Verify we received the notification that was sent while disconnected + expect(allText).toContain('Missed while disconnected'); + }, 15000); + }); + + // Test onsessionclosed callback + describe('NodeStreamableHTTPServerTransport onsessionclosed callback', () => { + it('should call onsessionclosed callback when session is closed via DELETE', async () => { + const mockCallback = vi.fn(); + + // Create server with onsessionclosed callback + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessionclosed: mockCallback + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize to get a session ID + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + const tempSessionId = initResponse.headers.get('mcp-session-id'); + expect(tempSessionId).toBeDefined(); + + // DELETE the session + const deleteResponse = await fetch(tempUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': tempSessionId || '', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(deleteResponse.status).toBe(200); + expect(mockCallback).toHaveBeenCalledWith(tempSessionId); + expect(mockCallback).toHaveBeenCalledTimes(1); + + // Clean up + tempServer.close(); + }); + + it('should not call onsessionclosed callback when not provided', async () => { + // Create server without onsessionclosed callback + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID() + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize to get a session ID + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + const tempSessionId = initResponse.headers.get('mcp-session-id'); + + // DELETE the session - should not throw error + const deleteResponse = await fetch(tempUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': tempSessionId || '', + 'mcp-protocol-version': '2025-03-26' + } + }); + + expect(deleteResponse.status).toBe(200); + + // Clean up + tempServer.close(); + }); + + it('should not call onsessionclosed callback for invalid session DELETE', async () => { + const mockCallback = vi.fn(); + + // Create server with onsessionclosed callback + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessionclosed: mockCallback + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize to get a valid session + await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + + // Try to DELETE with invalid session ID + const deleteResponse = await fetch(tempUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': 'invalid-session-id', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(deleteResponse.status).toBe(404); + expect(mockCallback).not.toHaveBeenCalled(); + + // Clean up + tempServer.close(); + }); + + it('should call onsessionclosed callback with correct session ID when multiple sessions exist', async () => { + const mockCallback = vi.fn(); + + // Create first server + const result1 = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessionclosed: mockCallback + }); + + const server1 = result1.server; + const url1 = result1.baseUrl; + + // Create second server + const result2 = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessionclosed: mockCallback + }); + + const server2 = result2.server; + const url2 = result2.baseUrl; + + // Initialize both servers + const initResponse1 = await sendPostRequest(url1, TEST_MESSAGES.initialize); + const sessionId1 = initResponse1.headers.get('mcp-session-id'); + + const initResponse2 = await sendPostRequest(url2, TEST_MESSAGES.initialize); + const sessionId2 = initResponse2.headers.get('mcp-session-id'); + + expect(sessionId1).toBeDefined(); + expect(sessionId2).toBeDefined(); + expect(sessionId1).not.toBe(sessionId2); + + // DELETE first session + const deleteResponse1 = await fetch(url1, { + method: 'DELETE', + headers: { + 'mcp-session-id': sessionId1 || '', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(deleteResponse1.status).toBe(200); + expect(mockCallback).toHaveBeenCalledWith(sessionId1); + expect(mockCallback).toHaveBeenCalledTimes(1); + + // DELETE second session + const deleteResponse2 = await fetch(url2, { + method: 'DELETE', + headers: { + 'mcp-session-id': sessionId2 || '', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(deleteResponse2.status).toBe(200); + expect(mockCallback).toHaveBeenCalledWith(sessionId2); + expect(mockCallback).toHaveBeenCalledTimes(2); + + // Clean up + server1.close(); + server2.close(); + }); + }); + + // Test async callbacks for onsessioninitialized and onsessionclosed + describe('NodeStreamableHTTPServerTransport async callbacks', () => { + it('should support async onsessioninitialized callback', async () => { + const initializationOrder: string[] = []; + + // Create server with async onsessioninitialized callback + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: async (sessionId: string) => { + initializationOrder.push('async-start'); + // Simulate async operation + await new Promise(resolve => setTimeout(resolve, 10)); + initializationOrder.push('async-end'); + initializationOrder.push(sessionId); + } + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize to trigger the callback + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + const tempSessionId = initResponse.headers.get('mcp-session-id'); + + // Give time for async callback to complete + await new Promise(resolve => setTimeout(resolve, 50)); + + expect(initializationOrder).toEqual(['async-start', 'async-end', tempSessionId]); + + // Clean up + tempServer.close(); + }); + + it('should support sync onsessioninitialized callback (backwards compatibility)', async () => { + const capturedSessionId: string[] = []; + + // Create server with sync onsessioninitialized callback + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId: string) => { + capturedSessionId.push(sessionId); + } + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize to trigger the callback + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + const tempSessionId = initResponse.headers.get('mcp-session-id'); + + expect(capturedSessionId).toEqual([tempSessionId]); + + // Clean up + tempServer.close(); + }); + + it('should support async onsessionclosed callback', async () => { + const closureOrder: string[] = []; + + // Create server with async onsessionclosed callback + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessionclosed: async (sessionId: string) => { + closureOrder.push('async-close-start'); + // Simulate async operation + await new Promise(resolve => setTimeout(resolve, 10)); + closureOrder.push('async-close-end'); + closureOrder.push(sessionId); + } + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize to get a session ID + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + const tempSessionId = initResponse.headers.get('mcp-session-id'); + expect(tempSessionId).toBeDefined(); + + // DELETE the session + const deleteResponse = await fetch(tempUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': tempSessionId || '', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(deleteResponse.status).toBe(200); + + // Give time for async callback to complete + await new Promise(resolve => setTimeout(resolve, 50)); + + expect(closureOrder).toEqual(['async-close-start', 'async-close-end', tempSessionId]); + + // Clean up + tempServer.close(); + }); + + it('should propagate errors from async onsessioninitialized callback', async () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + // Create server with async onsessioninitialized callback that throws + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: async (_sessionId: string) => { + throw new Error('Async initialization error'); + } + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize should fail when callback throws + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + expect(initResponse.status).toBe(400); + + // Clean up + consoleErrorSpy.mockRestore(); + tempServer.close(); + }); + + it('should propagate errors from async onsessionclosed callback', async () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + // Create server with async onsessionclosed callback that throws + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessionclosed: async (_sessionId: string) => { + throw new Error('Async closure error'); + } + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize to get a session ID + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + const tempSessionId = initResponse.headers.get('mcp-session-id'); + + // DELETE should fail when callback throws + const deleteResponse = await fetch(tempUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': tempSessionId || '', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(deleteResponse.status).toBe(500); + + // Clean up + consoleErrorSpy.mockRestore(); + tempServer.close(); + }); + + it('should handle both async callbacks together', async () => { + const events: string[] = []; + + // Create server with both async callbacks + const result = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: async (sessionId: string) => { + await new Promise(resolve => setTimeout(resolve, 5)); + events.push(`initialized:${sessionId}`); + }, + onsessionclosed: async (sessionId: string) => { + await new Promise(resolve => setTimeout(resolve, 5)); + events.push(`closed:${sessionId}`); + } + }); + + const tempServer = result.server; + const tempUrl = result.baseUrl; + + // Initialize to trigger first callback + const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); + const tempSessionId = initResponse.headers.get('mcp-session-id'); + + // Wait for async callback + await new Promise(resolve => setTimeout(resolve, 20)); + + expect(events).toContain(`initialized:${tempSessionId}`); + + // DELETE to trigger second callback + const deleteResponse = await fetch(tempUrl, { + method: 'DELETE', + headers: { + 'mcp-session-id': tempSessionId || '', + 'mcp-protocol-version': '2025-11-25' + } + }); + + expect(deleteResponse.status).toBe(200); + + // Wait for async callback + await new Promise(resolve => setTimeout(resolve, 20)); + + expect(events).toContain(`closed:${tempSessionId}`); + expect(events).toHaveLength(2); + + // Clean up + tempServer.close(); + }); + }); + + // Test DNS rebinding protection + describe('NodeStreamableHTTPServerTransport DNS rebinding protection', () => { + let server: Server; + let transport: NodeStreamableHTTPServerTransport; + let baseUrl: URL; + + afterEach(async () => { + if (server && transport) { + await stopTestServer({ server, transport }); + } + }); + + describe('Host header validation', () => { + it('should accept requests with allowed host headers', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedHosts: ['localhost'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + // Note: fetch() automatically sets Host header to match the URL + // Since we're connecting to localhost:3001 and that's in allowedHosts, this should work + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + expect(response.status).toBe(200); + }); + + it('should reject requests with disallowed host headers', async () => { + // Test DNS rebinding protection by creating a server that only allows example.com + // but we're connecting via localhost, so it should be rejected + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedHosts: ['example.com:3001'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + expect(response.status).toBe(403); + const body = (await response.json()) as JSONRPCErrorResponse; + expect(body.error.message).toContain('Invalid Host header:'); + }); + + it('should reject GET requests with disallowed host headers', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedHosts: ['example.com:3001'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + const response = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream' + } + }); + + expect(response.status).toBe(403); + }); + }); + + describe('Origin header validation', () => { + it('should accept requests with allowed origin headers', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedOrigins: ['http://localhost:3000', 'https://example.com'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Origin: 'http://localhost:3000' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + expect(response.status).toBe(200); + }); + + it('should reject requests with disallowed origin headers', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedOrigins: ['http://localhost:3000'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Origin: 'http://evil.com' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + expect(response.status).toBe(403); + const body = (await response.json()) as JSONRPCErrorResponse; + expect(body.error.message).toBe('Invalid Origin header: http://evil.com'); + }); + + it('should accept requests without origin headers', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedOrigins: ['http://localhost:3000', 'https://example.com'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + // Should pass even with no Origin headers because requests that do not come from browsers may not have Origin and DNS rebinding attacks can only be performed via browsers + expect(response.status).toBe(200); + }); + }); + + describe('enableDnsRebindingProtection option', () => { + it('should skip all validations when enableDnsRebindingProtection is false', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedHosts: ['localhost'], + allowedOrigins: ['http://localhost:3000'], + enableDnsRebindingProtection: false + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Host: 'evil.com', + Origin: 'http://evil.com' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + // Should pass even with invalid headers because protection is disabled + expect(response.status).toBe(200); + }); + }); + + describe('Combined validations', () => { + it('should validate both host and origin when both are configured', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedHosts: ['localhost'], + allowedOrigins: ['http://localhost:3001'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + // Test with invalid origin (host will be automatically correct via fetch) + const response1 = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Origin: 'http://evil.com' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + expect(response1.status).toBe(403); + const body1 = (await response1.json()) as JSONRPCErrorResponse; + expect(body1.error.message).toBe('Invalid Origin header: http://evil.com'); + + // Test with valid origin + const response2 = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Origin: 'http://localhost:3001' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + expect(response2.status).toBe(200); + }); + }); + }); +}); + +/** + * Helper to create test server with DNS rebinding protection options + */ +async function createTestServerWithDnsProtection(config: { + sessionIdGenerator: (() => string) | undefined; + allowedHosts?: string[]; + allowedOrigins?: string[]; + enableDnsRebindingProtection?: boolean; +}): Promise<{ + server: Server; + transport: NodeStreamableHTTPServerTransport; + mcpServer: McpServer; + baseUrl: URL; +}> { + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + + const port = await getFreePort(); + + if (config.allowedHosts) { + config.allowedHosts = config.allowedHosts.map(host => { + if (host.includes(':')) { + return host; + } + return `localhost:${port}`; + }); + } + + const transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: config.sessionIdGenerator, + allowedHosts: config.allowedHosts, + allowedOrigins: config.allowedOrigins, + enableDnsRebindingProtection: config.enableDnsRebindingProtection + }); + + await mcpServer.connect(transport); + + const httpServer = createServer(async (req, res) => { + if (req.method === 'POST') { + let body = ''; + req.on('data', chunk => (body += chunk)); + req.on('end', async () => { + const parsedBody = JSON.parse(body); + await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res, parsedBody); + }); + } else { + await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res); + } + }); + + await new Promise(resolve => { + httpServer.listen(port, () => resolve()); + }); + + const serverUrl = new URL(`http://localhost:${port}/`); + + return { + server: httpServer, + transport, + mcpServer, + baseUrl: serverUrl + }; +} diff --git a/packages/middleware/node/tsconfig.json b/packages/middleware/node/tsconfig.json new file mode 100644 index 000000000..97790e3e1 --- /dev/null +++ b/packages/middleware/node/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "*": ["./*"], + "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], + "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"] + } + } +} diff --git a/packages/middleware/node/tsdown.config.ts b/packages/middleware/node/tsdown.config.ts new file mode 100644 index 000000000..c3d38817a --- /dev/null +++ b/packages/middleware/node/tsdown.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + // 1. Entry Points + // Directly matches package.json include/exclude globs + entry: ['src/index.ts'], + + // 2. Output Configuration + format: ['esm'], + outDir: 'dist', + clean: true, // Recommended: Cleans 'dist' before building + sourcemap: true, + + // 3. Platform & Target + target: 'esnext', + platform: 'node', + shims: true, // Polyfills common Node.js shims (__dirname, etc.) + + // 4. Type Definitions + // Bundles d.ts files into a single output + dts: { + resolver: 'tsc', + // override just for DTS generation: + compilerOptions: { + baseUrl: '.', + paths: { + '@modelcontextprotocol/core': ['../core/src/index.ts'] + } + } + }, + // 5. Vendoring Strategy - Bundle the code for this specific package into the output, + // but treat all other dependencies as external (require/import). + noExternal: ['@modelcontextprotocol/core'] +}); diff --git a/packages/middleware/node/vitest.config.js b/packages/middleware/node/vitest.config.js new file mode 100644 index 000000000..496fca320 --- /dev/null +++ b/packages/middleware/node/vitest.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; diff --git a/packages/server/package.json b/packages/server/package.json index 20bd77aae..d6c78541a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@modelcontextprotocol/server", "version": "2.0.0-alpha.0", - "description": "Model Context Protocol implementation for TypeScript", + "description": "Model Context Protocol implementation for TypeScript - Server package", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", "homepage": "https://modelcontextprotocol.io", @@ -18,7 +18,8 @@ "packageManager": "pnpm@10.24.0", "keywords": [ "modelcontextprotocol", - "mcp" + "mcp", + "server" ], "exports": { ".": { @@ -44,10 +45,6 @@ "client": "tsx scripts/cli.ts client" }, "dependencies": { - "@hono/node-server": "catalog:runtimeServerOnly", - "content-type": "catalog:runtimeServerOnly", - "pkce-challenge": "catalog:runtimeShared", - "raw-body": "catalog:runtimeServerOnly", "zod": "catalog:runtimeShared", "zod-to-json-schema": "catalog:runtimeShared" }, @@ -71,13 +68,8 @@ "@modelcontextprotocol/test-helpers": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", - "@types/content-type": "catalog:devTools", - "@types/cors": "catalog:devTools", "@types/cross-spawn": "catalog:devTools", "@types/eventsource": "catalog:devTools", - "@types/express": "catalog:devTools", - "@types/express-serve-static-core": "catalog:devTools", - "@types/supertest": "catalog:devTools", "@typescript/native-preview": "catalog:devTools", "eslint": "catalog:devTools", "eslint-config-prettier": "catalog:devTools", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 52ec221e2..f45be3961 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,3 +1,4 @@ +export * from '../../middleware/node/src/streamableHttp.js'; export * from './server/completable.js'; export * from './server/helper/body.js'; export * from './server/mcp.js'; @@ -5,7 +6,6 @@ export * from './server/middleware/hostHeaderValidation.js'; export * from './server/server.js'; export * from './server/stdio.js'; export * from './server/streamableHttp.js'; -export * from './server/webStandardStreamableHttp.js'; // experimental exports export * from './experimental/index.js'; diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index 10a990196..7034ce64a 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -1,51 +1,195 @@ /** - * Node.js HTTP Streamable HTTP Server Transport + * Web Standards Streamable HTTP Server Transport * - * This is a thin wrapper around `WebStandardStreamableHTTPServerTransport` that provides - * compatibility with Node.js HTTP server (IncomingMessage/ServerResponse). + * This is the core transport implementation using Web Standard APIs (Request, Response, ReadableStream). + * It can run on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc. * - * For web-standard environments (Cloudflare Workers, Deno, Bun), use `WebStandardStreamableHTTPServerTransport` directly. + * For Node.js Express/HTTP compatibility, use `NodeStreamableHTTPServerTransport` which wraps this transport. */ -import type { IncomingMessage, ServerResponse } from 'node:http'; +import { TextEncoder } from 'node:util'; -import { getRequestListener } from '@hono/node-server'; -import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport } from '@modelcontextprotocol/core'; +import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, RequestInfo, Transport } from '@modelcontextprotocol/core'; +import { + DEFAULT_NEGOTIATED_PROTOCOL_VERSION, + isInitializeRequest, + isJSONRPCErrorResponse, + isJSONRPCRequest, + isJSONRPCResultResponse, + JSONRPCMessageSchema, + SUPPORTED_PROTOCOL_VERSIONS +} from '@modelcontextprotocol/core'; -import type { WebStandardStreamableHTTPServerTransportOptions } from './webStandardStreamableHttp.js'; -import { WebStandardStreamableHTTPServerTransport } from './webStandardStreamableHttp.js'; +export type StreamId = string; +export type EventId = string; /** - * Configuration options for StreamableHTTPServerTransport - * - * This is an alias for WebStandardStreamableHTTPServerTransportOptions for backward compatibility. + * Interface for resumability support via event storage + */ +export interface EventStore { + /** + * Stores an event for later retrieval + * @param streamId ID of the stream the event belongs to + * @param message The JSON-RPC message to store + * @returns The generated event ID for the stored event + */ + storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise; + + /** + * Get the stream ID associated with a given event ID. + * @param eventId The event ID to look up + * @returns The stream ID, or undefined if not found + * + * Optional: If not provided, the SDK will use the streamId returned by + * replayEventsAfter for stream mapping. + */ + getStreamIdForEventId?(eventId: EventId): Promise; + + replayEventsAfter( + lastEventId: EventId, + { + send + }: { + send: (eventId: EventId, message: JSONRPCMessage) => Promise; + } + ): Promise; +} + +/** + * Internal stream mapping for managing SSE connections + */ +interface StreamMapping { + /** Stream controller for pushing SSE data - only used with ReadableStream approach */ + controller?: ReadableStreamDefaultController; + /** Text encoder for SSE formatting */ + encoder?: TextEncoder; + /** Promise resolver for JSON response mode */ + resolveJson?: (response: Response) => void; + /** Cleanup function to close stream and remove mapping */ + cleanup: () => void; +} + +/** + * Configuration options for WebStandardStreamableHTTPServerTransport + */ +export interface WebStandardStreamableHTTPServerTransportOptions { + /** + * Function that generates a session ID for the transport. + * The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash) + * + * If not provided, session management is disabled (stateless mode). + */ + sessionIdGenerator?: () => string; + + /** + * A callback for session initialization events + * This is called when the server initializes a new session. + * Useful in cases when you need to register multiple mcp sessions + * and need to keep track of them. + * @param sessionId The generated session ID + */ + onsessioninitialized?: (sessionId: string) => void | Promise; + + /** + * A callback for session close events + * This is called when the server closes a session due to a DELETE request. + * Useful in cases when you need to clean up resources associated with the session. + * Note that this is different from the transport closing, if you are handling + * HTTP requests from multiple nodes you might want to close each + * WebStandardStreamableHTTPServerTransport after a request is completed while still keeping the + * session open/running. + * @param sessionId The session ID that was closed + */ + onsessionclosed?: (sessionId: string) => void | Promise; + + /** + * If true, the server will return JSON responses instead of starting an SSE stream. + * This can be useful for simple request/response scenarios without streaming. + * Default is false (SSE streams are preferred). + */ + enableJsonResponse?: boolean; + + /** + * Event store for resumability support + * If provided, resumability will be enabled, allowing clients to reconnect and resume messages + */ + eventStore?: EventStore; + + /** + * List of allowed host header values for DNS rebinding protection. + * If not specified, host validation is disabled. + * @deprecated Use external middleware for host validation instead. + */ + allowedHosts?: string[]; + + /** + * List of allowed origin header values for DNS rebinding protection. + * If not specified, origin validation is disabled. + * @deprecated Use external middleware for origin validation instead. + */ + allowedOrigins?: string[]; + + /** + * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). + * Default is false for backwards compatibility. + * @deprecated Use external middleware for DNS rebinding protection instead. + */ + enableDnsRebindingProtection?: boolean; + + /** + * Retry interval in milliseconds to suggest to clients in SSE retry field. + * When set, the server will send a retry field in SSE priming events to control + * client reconnection timing for polling behavior. + */ + retryInterval?: number; +} + +/** + * Options for handling a request */ -export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions; +export interface HandleRequestOptions { + /** + * Pre-parsed request body. If provided, the transport will use this instead of parsing req.json(). + * Useful when using body-parser middleware that has already parsed the body. + */ + parsedBody?: unknown; + + /** + * Authentication info from middleware. If provided, will be passed to message handlers. + */ + authInfo?: AuthInfo; +} /** - * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. - * It supports both SSE streaming and direct HTTP responses. + * Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification + * using Web Standard APIs (Request, Response, ReadableStream). * - * This is a wrapper around `WebStandardStreamableHTTPServerTransport` that provides Node.js HTTP compatibility. - * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs. + * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc. * * Usage example: * * ```typescript * // Stateful mode - server sets the session ID - * const statefulTransport = new StreamableHTTPServerTransport({ - * sessionIdGenerator: () => randomUUID(), + * const statefulTransport = new WebStandardStreamableHTTPServerTransport({ + * sessionIdGenerator: () => crypto.randomUUID(), * }); * * // Stateless mode - explicitly set session ID to undefined - * const statelessTransport = new StreamableHTTPServerTransport({ + * const statelessTransport = new WebStandardStreamableHTTPServerTransport({ * sessionIdGenerator: undefined, * }); * - * // Using with pre-parsed request body - * app.post('/mcp', (req, res) => { - * transport.handleRequest(req, res, req.body); + * // Hono.js usage + * app.all('/mcp', async (c) => { + * return transport.handleRequest(c.req.raw); * }); + * + * // Cloudflare Workers usage + * export default { + * async fetch(request: Request): Promise { + * return transport.handleRequest(request); + * } + * }; * ``` * * In stateful mode: @@ -59,115 +203,674 @@ export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServ * - No Session ID is included in any responses * - No session validation is performed */ -export class NodeStreamableHTTPServerTransport implements Transport { - private _webStandardTransport: WebStandardStreamableHTTPServerTransport; - private _requestListener: ReturnType; - // Store auth and parsedBody per request for passing through to handleRequest - private _requestContext: WeakMap = new WeakMap(); - - constructor(options: StreamableHTTPServerTransportOptions = {}) { - this._webStandardTransport = new WebStandardStreamableHTTPServerTransport(options); - - // Create a request listener that wraps the web standard transport - // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming - this._requestListener = getRequestListener(async (webRequest: Request) => { - // Get context if available (set during handleRequest) - const context = this._requestContext.get(webRequest); - return this._webStandardTransport.handleRequest(webRequest, { - authInfo: context?.authInfo, - parsedBody: context?.parsedBody - }); - }); +export class WebStandardStreamableHTTPServerTransport implements Transport { + // when sessionId is not set (undefined), it means the transport is in stateless mode + private sessionIdGenerator: (() => string) | undefined; + private _started: boolean = false; + private _streamMapping: Map = new Map(); + private _requestToStreamMapping: Map = new Map(); + private _requestResponseMap: Map = new Map(); + private _initialized: boolean = false; + private _enableJsonResponse: boolean = false; + private _standaloneSseStreamId: string = '_GET_stream'; + private _eventStore?: EventStore; + private _onsessioninitialized?: (sessionId: string) => void | Promise; + private _onsessionclosed?: (sessionId: string) => void | Promise; + private _allowedHosts?: string[]; + private _allowedOrigins?: string[]; + private _enableDnsRebindingProtection: boolean; + private _retryInterval?: number; + + sessionId?: string; + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; + + constructor(options: WebStandardStreamableHTTPServerTransportOptions = {}) { + this.sessionIdGenerator = options.sessionIdGenerator; + this._enableJsonResponse = options.enableJsonResponse ?? false; + this._eventStore = options.eventStore; + this._onsessioninitialized = options.onsessioninitialized; + this._onsessionclosed = options.onsessionclosed; + this._allowedHosts = options.allowedHosts; + this._allowedOrigins = options.allowedOrigins; + this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false; + this._retryInterval = options.retryInterval; } /** - * Gets the session ID for this transport instance. + * Starts the transport. This is required by the Transport interface but is a no-op + * for the Streamable HTTP transport as connections are managed per-request. */ - get sessionId(): string | undefined { - return this._webStandardTransport.sessionId; + async start(): Promise { + if (this._started) { + throw new Error('Transport already started'); + } + this._started = true; } /** - * Sets callback for when the transport is closed. + * Helper to create a JSON error response */ - set onclose(handler: (() => void) | undefined) { - this._webStandardTransport.onclose = handler; + private createJsonErrorResponse( + status: number, + code: number, + message: string, + options?: { headers?: Record; data?: string } + ): Response { + const error: { code: number; message: string; data?: string } = { code, message }; + if (options?.data !== undefined) { + error.data = options.data; + } + return new Response( + JSON.stringify({ + jsonrpc: '2.0', + error, + id: null + }), + { + status, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + } + ); } - get onclose(): (() => void) | undefined { - return this._webStandardTransport.onclose; + /** + * Validates request headers for DNS rebinding protection. + * @returns Error response if validation fails, undefined if validation passes. + */ + private validateRequestHeaders(req: Request): Response | undefined { + // Skip validation if protection is not enabled + if (!this._enableDnsRebindingProtection) { + return undefined; + } + + // Validate Host header if allowedHosts is configured + if (this._allowedHosts && this._allowedHosts.length > 0) { + const hostHeader = req.headers.get('host'); + if (!hostHeader || !this._allowedHosts.includes(hostHeader)) { + const error = `Invalid Host header: ${hostHeader}`; + this.onerror?.(new Error(error)); + return this.createJsonErrorResponse(403, -32000, error); + } + } + + // Validate Origin header if allowedOrigins is configured + if (this._allowedOrigins && this._allowedOrigins.length > 0) { + const originHeader = req.headers.get('origin'); + if (originHeader && !this._allowedOrigins.includes(originHeader)) { + const error = `Invalid Origin header: ${originHeader}`; + this.onerror?.(new Error(error)); + return this.createJsonErrorResponse(403, -32000, error); + } + } + + return undefined; + } + + /** + * Handles an incoming HTTP request, whether GET, POST, or DELETE + * Returns a Response object (Web Standard) + */ + async handleRequest(req: Request, options?: HandleRequestOptions): Promise { + // Validate request headers for DNS rebinding protection + const validationError = this.validateRequestHeaders(req); + if (validationError) { + return validationError; + } + + switch (req.method) { + case 'POST': + return this.handlePostRequest(req, options); + case 'GET': + return this.handleGetRequest(req); + case 'DELETE': + return this.handleDeleteRequest(req); + default: + return this.handleUnsupportedRequest(); + } + } + + /** + * Writes a priming event to establish resumption capability. + * Only sends if eventStore is configured (opt-in for resumability) and + * the client's protocol version supports empty SSE data (>= 2025-11-25). + */ + private async writePrimingEvent( + controller: ReadableStreamDefaultController, + encoder: TextEncoder, + streamId: string, + protocolVersion: string + ): Promise { + if (!this._eventStore) { + return; + } + + // Priming events have empty data which older clients cannot handle. + // Only send priming events to clients with protocol version >= 2025-11-25 + // which includes the fix for handling empty SSE data. + if (protocolVersion < '2025-11-25') { + return; + } + + const primingEventId = await this._eventStore.storeEvent(streamId, {} as JSONRPCMessage); + + let primingEvent = `id: ${primingEventId}\ndata: \n\n`; + if (this._retryInterval !== undefined) { + primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`; + } + controller.enqueue(encoder.encode(primingEvent)); } /** - * Sets callback for transport errors. + * Handles GET requests for SSE stream */ - set onerror(handler: ((error: Error) => void) | undefined) { - this._webStandardTransport.onerror = handler; + private async handleGetRequest(req: Request): Promise { + // The client MUST include an Accept header, listing text/event-stream as a supported content type. + const acceptHeader = req.headers.get('accept'); + if (!acceptHeader?.includes('text/event-stream')) { + return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept text/event-stream'); + } + + // If an Mcp-Session-Id is returned by the server during initialization, + // clients using the Streamable HTTP transport MUST include it + // in the Mcp-Session-Id header on all of their subsequent HTTP requests. + const sessionError = this.validateSession(req); + if (sessionError) { + return sessionError; + } + const protocolError = this.validateProtocolVersion(req); + if (protocolError) { + return protocolError; + } + + // Handle resumability: check for Last-Event-ID header + if (this._eventStore) { + const lastEventId = req.headers.get('last-event-id'); + if (lastEventId) { + return this.replayEvents(lastEventId); + } + } + + // Check if there's already an active standalone SSE stream for this session + if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) { + // Only one GET SSE stream is allowed per session + return this.createJsonErrorResponse(409, -32000, 'Conflict: Only one SSE stream is allowed per session'); + } + + const encoder = new TextEncoder(); + let streamController: ReadableStreamDefaultController; + + // Create a ReadableStream with a controller we can use to push SSE events + const readable = new ReadableStream({ + start: controller => { + streamController = controller; + }, + cancel: () => { + // Stream was cancelled by client + this._streamMapping.delete(this._standaloneSseStreamId); + } + }); + + const headers: Record = { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + Connection: 'keep-alive' + }; + + // After initialization, always include the session ID if we have one + if (this.sessionId !== undefined) { + headers['mcp-session-id'] = this.sessionId; + } + + // Store the stream mapping with the controller for pushing data + this._streamMapping.set(this._standaloneSseStreamId, { + controller: streamController!, + encoder, + cleanup: () => { + this._streamMapping.delete(this._standaloneSseStreamId); + try { + streamController!.close(); + } catch { + // Controller might already be closed + } + } + }); + + return new Response(readable, { headers }); } - get onerror(): ((error: Error) => void) | undefined { - return this._webStandardTransport.onerror; + /** + * Replays events that would have been sent after the specified event ID + * Only used when resumability is enabled + */ + private async replayEvents(lastEventId: string): Promise { + if (!this._eventStore) { + return this.createJsonErrorResponse(400, -32000, 'Event store not configured'); + } + + try { + // If getStreamIdForEventId is available, use it for conflict checking + let streamId: string | undefined; + if (this._eventStore.getStreamIdForEventId) { + streamId = await this._eventStore.getStreamIdForEventId(lastEventId); + + if (!streamId) { + return this.createJsonErrorResponse(400, -32000, 'Invalid event ID format'); + } + + // Check conflict with the SAME streamId we'll use for mapping + if (this._streamMapping.get(streamId) !== undefined) { + return this.createJsonErrorResponse(409, -32000, 'Conflict: Stream already has an active connection'); + } + } + + const headers: Record = { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + Connection: 'keep-alive' + }; + + if (this.sessionId !== undefined) { + headers['mcp-session-id'] = this.sessionId; + } + + // Create a ReadableStream with controller for SSE + const encoder = new TextEncoder(); + let streamController: ReadableStreamDefaultController; + + const readable = new ReadableStream({ + start: controller => { + streamController = controller; + }, + cancel: () => { + // Stream was cancelled by client + // Cleanup will be handled by the mapping + } + }); + + // Replay events - returns the streamId for backwards compatibility + const replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, { + send: async (eventId: string, message: JSONRPCMessage) => { + const success = this.writeSSEEvent(streamController!, encoder, message, eventId); + if (!success) { + this.onerror?.(new Error('Failed replay events')); + try { + streamController!.close(); + } catch { + // Controller might already be closed + } + } + } + }); + + this._streamMapping.set(replayedStreamId, { + controller: streamController!, + encoder, + cleanup: () => { + this._streamMapping.delete(replayedStreamId); + try { + streamController!.close(); + } catch { + // Controller might already be closed + } + } + }); + + return new Response(readable, { headers }); + } catch (error) { + this.onerror?.(error as Error); + return this.createJsonErrorResponse(500, -32000, 'Error replaying events'); + } } /** - * Sets callback for incoming messages. + * Writes an event to an SSE stream via controller with proper formatting */ - set onmessage(handler: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined) { - this._webStandardTransport.onmessage = handler; + private writeSSEEvent( + controller: ReadableStreamDefaultController, + encoder: TextEncoder, + message: JSONRPCMessage, + eventId?: string + ): boolean { + try { + let eventData = `event: message\n`; + // Include event ID if provided - this is important for resumability + if (eventId) { + eventData += `id: ${eventId}\n`; + } + eventData += `data: ${JSON.stringify(message)}\n\n`; + controller.enqueue(encoder.encode(eventData)); + return true; + } catch { + return false; + } } - get onmessage(): ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined { - return this._webStandardTransport.onmessage; + /** + * Handles unsupported requests (PUT, PATCH, etc.) + */ + private handleUnsupportedRequest(): Response { + return new Response( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed.' + }, + id: null + }), + { + status: 405, + headers: { + Allow: 'GET, POST, DELETE', + 'Content-Type': 'application/json' + } + } + ); } /** - * Starts the transport. This is required by the Transport interface but is a no-op - * for the Streamable HTTP transport as connections are managed per-request. + * Handles POST requests containing JSON-RPC messages */ - async start(): Promise { - return this._webStandardTransport.start(); + private async handlePostRequest(req: Request, options?: HandleRequestOptions): Promise { + try { + // Validate the Accept header + const acceptHeader = req.headers.get('accept'); + // The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types. + if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) { + return this.createJsonErrorResponse( + 406, + -32000, + 'Not Acceptable: Client must accept both application/json and text/event-stream' + ); + } + + const ct = req.headers.get('content-type'); + if (!ct || !ct.includes('application/json')) { + return this.createJsonErrorResponse(415, -32000, 'Unsupported Media Type: Content-Type must be application/json'); + } + + // Build request info from headers + const requestInfo: RequestInfo = { + headers: Object.fromEntries(req.headers.entries()) + }; + + let rawMessage; + if (options?.parsedBody !== undefined) { + rawMessage = options.parsedBody; + } else { + try { + rawMessage = await req.json(); + } catch { + return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON'); + } + } + + let messages: JSONRPCMessage[]; + + // handle batch and single messages + try { + if (Array.isArray(rawMessage)) { + messages = rawMessage.map(msg => JSONRPCMessageSchema.parse(msg)); + } else { + messages = [JSONRPCMessageSchema.parse(rawMessage)]; + } + } catch { + return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON-RPC message'); + } + + // Check if this is an initialization request + // https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle/ + const isInitializationRequest = messages.some(isInitializeRequest); + if (isInitializationRequest) { + // If it's a server with session management and the session ID is already set we should reject the request + // to avoid re-initialization. + if (this._initialized && this.sessionId !== undefined) { + return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Server already initialized'); + } + if (messages.length > 1) { + return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Only one initialization request is allowed'); + } + this.sessionId = this.sessionIdGenerator?.(); + this._initialized = true; + + // If we have a session ID and an onsessioninitialized handler, call it immediately + // This is needed in cases where the server needs to keep track of multiple sessions + if (this.sessionId && this._onsessioninitialized) { + await Promise.resolve(this._onsessioninitialized(this.sessionId)); + } + } + if (!isInitializationRequest) { + // If an Mcp-Session-Id is returned by the server during initialization, + // clients using the Streamable HTTP transport MUST include it + // in the Mcp-Session-Id header on all of their subsequent HTTP requests. + const sessionError = this.validateSession(req); + if (sessionError) { + return sessionError; + } + // Mcp-Protocol-Version header is required for all requests after initialization. + const protocolError = this.validateProtocolVersion(req); + if (protocolError) { + return protocolError; + } + } + + // check if it contains requests + const hasRequests = messages.some(isJSONRPCRequest); + + if (!hasRequests) { + // if it only contains notifications or responses, return 202 + for (const message of messages) { + this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo }); + } + return new Response(null, { + status: 202, + headers: { + 'Content-Type': 'application/json' + } + }); + } + + // The default behavior is to use SSE streaming + // but in some cases server will return JSON responses + const streamId = crypto.randomUUID(); + + // Extract protocol version for priming event decision. + // For initialize requests, get from request params. + // For other requests, get from header (already validated). + const initRequest = messages.find(m => isInitializeRequest(m)); + const clientProtocolVersion = initRequest + ? initRequest.params.protocolVersion + : (req.headers.get('mcp-protocol-version') ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION); + + if (this._enableJsonResponse) { + // For JSON response mode, return a Promise that resolves when all responses are ready + return new Promise(resolve => { + this._streamMapping.set(streamId, { + resolveJson: resolve, + cleanup: () => { + this._streamMapping.delete(streamId); + } + }); + + for (const message of messages) { + if (isJSONRPCRequest(message)) { + this._requestToStreamMapping.set(message.id, streamId); + } + } + + for (const message of messages) { + this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo }); + } + }); + } + + // SSE streaming mode - use ReadableStream with controller for more reliable data pushing + const encoder = new TextEncoder(); + let streamController: ReadableStreamDefaultController; + + const readable = new ReadableStream({ + start: controller => { + streamController = controller; + }, + cancel: () => { + // Stream was cancelled by client + this._streamMapping.delete(streamId); + } + }); + + const headers: Record = { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + }; + + // After initialization, always include the session ID if we have one + if (this.sessionId !== undefined) { + headers['mcp-session-id'] = this.sessionId; + } + + // Store the response for this request to send messages back through this connection + // We need to track by request ID to maintain the connection + for (const message of messages) { + if (isJSONRPCRequest(message)) { + this._streamMapping.set(streamId, { + controller: streamController!, + encoder, + cleanup: () => { + this._streamMapping.delete(streamId); + try { + streamController!.close(); + } catch { + // Controller might already be closed + } + } + }); + this._requestToStreamMapping.set(message.id, streamId); + } + } + + // Write priming event if event store is configured (after mapping is set up) + await this.writePrimingEvent(streamController!, encoder, streamId, clientProtocolVersion); + + // handle each message + for (const message of messages) { + // Build closeSSEStream callback for requests when eventStore is configured + // AND client supports resumability (protocol version >= 2025-11-25). + // Old clients can't resume if the stream is closed early because they + // didn't receive a priming event with an event ID. + let closeSSEStream: (() => void) | undefined; + let closeStandaloneSSEStream: (() => void) | undefined; + if (isJSONRPCRequest(message) && this._eventStore && clientProtocolVersion >= '2025-11-25') { + closeSSEStream = () => { + this.closeSSEStream(message.id); + }; + closeStandaloneSSEStream = () => { + this.closeStandaloneSSEStream(); + }; + } + + this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo, closeSSEStream, closeStandaloneSSEStream }); + } + // The server SHOULD NOT close the SSE stream before sending all JSON-RPC responses + // This will be handled by the send() method when responses are ready + + return new Response(readable, { status: 200, headers }); + } catch (error) { + // return JSON-RPC formatted error + this.onerror?.(error as Error); + return this.createJsonErrorResponse(400, -32700, 'Parse error', { data: String(error) }); + } } /** - * Closes the transport and all active connections. + * Handles DELETE requests to terminate sessions */ - async close(): Promise { - return this._webStandardTransport.close(); + private async handleDeleteRequest(req: Request): Promise { + const sessionError = this.validateSession(req); + if (sessionError) { + return sessionError; + } + const protocolError = this.validateProtocolVersion(req); + if (protocolError) { + return protocolError; + } + + await Promise.resolve(this._onsessionclosed?.(this.sessionId!)); + await this.close(); + return new Response(null, { status: 200 }); } /** - * Sends a JSON-RPC message through the transport. + * Validates session ID for non-initialization requests. + * Returns Response error if invalid, undefined otherwise */ - async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise { - return this._webStandardTransport.send(message, options); + private validateSession(req: Request): Response | undefined { + if (this.sessionIdGenerator === undefined) { + // If the sessionIdGenerator ID is not set, the session management is disabled + // and we don't need to validate the session ID + return undefined; + } + if (!this._initialized) { + // If the server has not been initialized yet, reject all requests + return this.createJsonErrorResponse(400, -32000, 'Bad Request: Server not initialized'); + } + + const sessionId = req.headers.get('mcp-session-id'); + + if (!sessionId) { + // Non-initialization requests without a session ID should return 400 Bad Request + return this.createJsonErrorResponse(400, -32000, 'Bad Request: Mcp-Session-Id header is required'); + } + + if (sessionId !== this.sessionId) { + // Reject requests with invalid session ID with 404 Not Found + return this.createJsonErrorResponse(404, -32001, 'Session not found'); + } + + return undefined; } /** - * Handles an incoming HTTP request, whether GET or POST. + * Validates the MCP-Protocol-Version header on incoming requests. * - * This method converts Node.js HTTP objects to Web Standard Request/Response - * and delegates to the underlying WebStandardStreamableHTTPServerTransport. + * For initialization: Version negotiation handles unknown versions gracefully + * (server responds with its supported version). * - * @param req - Node.js IncomingMessage, optionally with auth property from middleware - * @param res - Node.js ServerResponse - * @param parsedBody - Optional pre-parsed body from body-parser middleware - */ - async handleRequest(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise { - // Store context for this request to pass through auth and parsedBody - // We need to intercept the request creation to attach this context - const authInfo = req.auth; - - // Create a custom handler that includes our context - const handler = getRequestListener(async (webRequest: Request) => { - return this._webStandardTransport.handleRequest(webRequest, { - authInfo, - parsedBody - }); + * For subsequent requests with MCP-Protocol-Version header: + * - Accept if in supported list + * - 400 if unsupported + * + * For HTTP requests without the MCP-Protocol-Version header: + * - Accept and default to the version negotiated at initialization + */ + private validateProtocolVersion(req: Request): Response | undefined { + const protocolVersion = req.headers.get('mcp-protocol-version'); + + if (protocolVersion !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) { + return this.createJsonErrorResponse( + 400, + -32000, + `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})` + ); + } + return undefined; + } + + async close(): Promise { + // Close all SSE connections + this._streamMapping.forEach(({ cleanup }) => { + cleanup(); }); + this._streamMapping.clear(); - // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion - // including proper SSE streaming support - await handler(req, res); + // Clear any pending responses + this._requestResponseMap.clear(); + this.onclose?.(); } /** @@ -176,7 +879,13 @@ export class NodeStreamableHTTPServerTransport implements Transport { * client will reconnect after the retry interval specified in the priming event. */ closeSSEStream(requestId: RequestId): void { - this._webStandardTransport.closeSSEStream(requestId); + const streamId = this._requestToStreamMapping.get(requestId); + if (!streamId) return; + + const stream = this._streamMapping.get(streamId); + if (stream) { + stream.cleanup(); + } } /** @@ -184,6 +893,107 @@ export class NodeStreamableHTTPServerTransport implements Transport { * Use this to implement polling behavior for server-initiated notifications. */ closeStandaloneSSEStream(): void { - this._webStandardTransport.closeStandaloneSSEStream(); + const stream = this._streamMapping.get(this._standaloneSseStreamId); + if (stream) { + stream.cleanup(); + } + } + + async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise { + let requestId = options?.relatedRequestId; + if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { + // If the message is a response, use the request ID from the message + requestId = message.id; + } + + // Check if this message should be sent on the standalone SSE stream (no request ID) + // Ignore notifications from tools (which have relatedRequestId set) + // Those will be sent via dedicated response SSE streams + if (requestId === undefined) { + // For standalone SSE streams, we can only send requests and notifications + if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { + throw new Error('Cannot send a response on a standalone SSE stream unless resuming a previous client request'); + } + + // Generate and store event ID if event store is provided + // Store even if stream is disconnected so events can be replayed on reconnect + let eventId: string | undefined; + if (this._eventStore) { + // Stores the event and gets the generated event ID + eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message); + } + + const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId); + if (standaloneSse === undefined) { + // Stream is disconnected - event is stored for replay, nothing more to do + return; + } + + // Send the message to the standalone SSE stream + if (standaloneSse.controller && standaloneSse.encoder) { + this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId); + } + return; + } + + // Get the response for this request + const streamId = this._requestToStreamMapping.get(requestId); + if (!streamId) { + throw new Error(`No connection established for request ID: ${String(requestId)}`); + } + + const stream = this._streamMapping.get(streamId); + + if (!this._enableJsonResponse && stream?.controller && stream?.encoder) { + // For SSE responses, generate event ID if event store is provided + let eventId: string | undefined; + + if (this._eventStore) { + eventId = await this._eventStore.storeEvent(streamId, message); + } + // Write the event to the response stream + this.writeSSEEvent(stream.controller, stream.encoder, message, eventId); + } + + if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { + this._requestResponseMap.set(requestId, message); + const relatedIds = Array.from(this._requestToStreamMapping.entries()) + .filter(([_, sid]) => sid === streamId) + .map(([id]) => id); + + // Check if we have responses for all requests using this connection + const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id)); + + if (allResponsesReady) { + if (!stream) { + throw new Error(`No connection established for request ID: ${String(requestId)}`); + } + if (this._enableJsonResponse && stream.resolveJson) { + // All responses ready, send as JSON + const headers: Record = { + 'Content-Type': 'application/json' + }; + if (this.sessionId !== undefined) { + headers['mcp-session-id'] = this.sessionId; + } + + const responses = relatedIds.map(id => this._requestResponseMap.get(id)!); + + if (responses.length === 1) { + stream.resolveJson(new Response(JSON.stringify(responses[0]), { status: 200, headers })); + } else { + stream.resolveJson(new Response(JSON.stringify(responses), { status: 200, headers })); + } + } else { + // End the SSE stream + stream.cleanup(); + } + // Clean up + for (const id of relatedIds) { + this._requestResponseMap.delete(id); + this._requestToStreamMapping.delete(id); + } + } + } } } diff --git a/packages/server/src/server/webStandardStreamableHttp.ts b/packages/server/src/server/webStandardStreamableHttp.ts deleted file mode 100644 index 7034ce64a..000000000 --- a/packages/server/src/server/webStandardStreamableHttp.ts +++ /dev/null @@ -1,999 +0,0 @@ -/** - * Web Standards Streamable HTTP Server Transport - * - * This is the core transport implementation using Web Standard APIs (Request, Response, ReadableStream). - * It can run on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc. - * - * For Node.js Express/HTTP compatibility, use `NodeStreamableHTTPServerTransport` which wraps this transport. - */ - -import { TextEncoder } from 'node:util'; - -import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, RequestInfo, Transport } from '@modelcontextprotocol/core'; -import { - DEFAULT_NEGOTIATED_PROTOCOL_VERSION, - isInitializeRequest, - isJSONRPCErrorResponse, - isJSONRPCRequest, - isJSONRPCResultResponse, - JSONRPCMessageSchema, - SUPPORTED_PROTOCOL_VERSIONS -} from '@modelcontextprotocol/core'; - -export type StreamId = string; -export type EventId = string; - -/** - * Interface for resumability support via event storage - */ -export interface EventStore { - /** - * Stores an event for later retrieval - * @param streamId ID of the stream the event belongs to - * @param message The JSON-RPC message to store - * @returns The generated event ID for the stored event - */ - storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise; - - /** - * Get the stream ID associated with a given event ID. - * @param eventId The event ID to look up - * @returns The stream ID, or undefined if not found - * - * Optional: If not provided, the SDK will use the streamId returned by - * replayEventsAfter for stream mapping. - */ - getStreamIdForEventId?(eventId: EventId): Promise; - - replayEventsAfter( - lastEventId: EventId, - { - send - }: { - send: (eventId: EventId, message: JSONRPCMessage) => Promise; - } - ): Promise; -} - -/** - * Internal stream mapping for managing SSE connections - */ -interface StreamMapping { - /** Stream controller for pushing SSE data - only used with ReadableStream approach */ - controller?: ReadableStreamDefaultController; - /** Text encoder for SSE formatting */ - encoder?: TextEncoder; - /** Promise resolver for JSON response mode */ - resolveJson?: (response: Response) => void; - /** Cleanup function to close stream and remove mapping */ - cleanup: () => void; -} - -/** - * Configuration options for WebStandardStreamableHTTPServerTransport - */ -export interface WebStandardStreamableHTTPServerTransportOptions { - /** - * Function that generates a session ID for the transport. - * The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash) - * - * If not provided, session management is disabled (stateless mode). - */ - sessionIdGenerator?: () => string; - - /** - * A callback for session initialization events - * This is called when the server initializes a new session. - * Useful in cases when you need to register multiple mcp sessions - * and need to keep track of them. - * @param sessionId The generated session ID - */ - onsessioninitialized?: (sessionId: string) => void | Promise; - - /** - * A callback for session close events - * This is called when the server closes a session due to a DELETE request. - * Useful in cases when you need to clean up resources associated with the session. - * Note that this is different from the transport closing, if you are handling - * HTTP requests from multiple nodes you might want to close each - * WebStandardStreamableHTTPServerTransport after a request is completed while still keeping the - * session open/running. - * @param sessionId The session ID that was closed - */ - onsessionclosed?: (sessionId: string) => void | Promise; - - /** - * If true, the server will return JSON responses instead of starting an SSE stream. - * This can be useful for simple request/response scenarios without streaming. - * Default is false (SSE streams are preferred). - */ - enableJsonResponse?: boolean; - - /** - * Event store for resumability support - * If provided, resumability will be enabled, allowing clients to reconnect and resume messages - */ - eventStore?: EventStore; - - /** - * List of allowed host header values for DNS rebinding protection. - * If not specified, host validation is disabled. - * @deprecated Use external middleware for host validation instead. - */ - allowedHosts?: string[]; - - /** - * List of allowed origin header values for DNS rebinding protection. - * If not specified, origin validation is disabled. - * @deprecated Use external middleware for origin validation instead. - */ - allowedOrigins?: string[]; - - /** - * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). - * Default is false for backwards compatibility. - * @deprecated Use external middleware for DNS rebinding protection instead. - */ - enableDnsRebindingProtection?: boolean; - - /** - * Retry interval in milliseconds to suggest to clients in SSE retry field. - * When set, the server will send a retry field in SSE priming events to control - * client reconnection timing for polling behavior. - */ - retryInterval?: number; -} - -/** - * Options for handling a request - */ -export interface HandleRequestOptions { - /** - * Pre-parsed request body. If provided, the transport will use this instead of parsing req.json(). - * Useful when using body-parser middleware that has already parsed the body. - */ - parsedBody?: unknown; - - /** - * Authentication info from middleware. If provided, will be passed to message handlers. - */ - authInfo?: AuthInfo; -} - -/** - * Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification - * using Web Standard APIs (Request, Response, ReadableStream). - * - * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc. - * - * Usage example: - * - * ```typescript - * // Stateful mode - server sets the session ID - * const statefulTransport = new WebStandardStreamableHTTPServerTransport({ - * sessionIdGenerator: () => crypto.randomUUID(), - * }); - * - * // Stateless mode - explicitly set session ID to undefined - * const statelessTransport = new WebStandardStreamableHTTPServerTransport({ - * sessionIdGenerator: undefined, - * }); - * - * // Hono.js usage - * app.all('/mcp', async (c) => { - * return transport.handleRequest(c.req.raw); - * }); - * - * // Cloudflare Workers usage - * export default { - * async fetch(request: Request): Promise { - * return transport.handleRequest(request); - * } - * }; - * ``` - * - * In stateful mode: - * - Session ID is generated and included in response headers - * - Session ID is always included in initialization responses - * - Requests with invalid session IDs are rejected with 404 Not Found - * - Non-initialization requests without a session ID are rejected with 400 Bad Request - * - State is maintained in-memory (connections, message history) - * - * In stateless mode: - * - No Session ID is included in any responses - * - No session validation is performed - */ -export class WebStandardStreamableHTTPServerTransport implements Transport { - // when sessionId is not set (undefined), it means the transport is in stateless mode - private sessionIdGenerator: (() => string) | undefined; - private _started: boolean = false; - private _streamMapping: Map = new Map(); - private _requestToStreamMapping: Map = new Map(); - private _requestResponseMap: Map = new Map(); - private _initialized: boolean = false; - private _enableJsonResponse: boolean = false; - private _standaloneSseStreamId: string = '_GET_stream'; - private _eventStore?: EventStore; - private _onsessioninitialized?: (sessionId: string) => void | Promise; - private _onsessionclosed?: (sessionId: string) => void | Promise; - private _allowedHosts?: string[]; - private _allowedOrigins?: string[]; - private _enableDnsRebindingProtection: boolean; - private _retryInterval?: number; - - sessionId?: string; - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; - - constructor(options: WebStandardStreamableHTTPServerTransportOptions = {}) { - this.sessionIdGenerator = options.sessionIdGenerator; - this._enableJsonResponse = options.enableJsonResponse ?? false; - this._eventStore = options.eventStore; - this._onsessioninitialized = options.onsessioninitialized; - this._onsessionclosed = options.onsessionclosed; - this._allowedHosts = options.allowedHosts; - this._allowedOrigins = options.allowedOrigins; - this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false; - this._retryInterval = options.retryInterval; - } - - /** - * Starts the transport. This is required by the Transport interface but is a no-op - * for the Streamable HTTP transport as connections are managed per-request. - */ - async start(): Promise { - if (this._started) { - throw new Error('Transport already started'); - } - this._started = true; - } - - /** - * Helper to create a JSON error response - */ - private createJsonErrorResponse( - status: number, - code: number, - message: string, - options?: { headers?: Record; data?: string } - ): Response { - const error: { code: number; message: string; data?: string } = { code, message }; - if (options?.data !== undefined) { - error.data = options.data; - } - return new Response( - JSON.stringify({ - jsonrpc: '2.0', - error, - id: null - }), - { - status, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - } - ); - } - - /** - * Validates request headers for DNS rebinding protection. - * @returns Error response if validation fails, undefined if validation passes. - */ - private validateRequestHeaders(req: Request): Response | undefined { - // Skip validation if protection is not enabled - if (!this._enableDnsRebindingProtection) { - return undefined; - } - - // Validate Host header if allowedHosts is configured - if (this._allowedHosts && this._allowedHosts.length > 0) { - const hostHeader = req.headers.get('host'); - if (!hostHeader || !this._allowedHosts.includes(hostHeader)) { - const error = `Invalid Host header: ${hostHeader}`; - this.onerror?.(new Error(error)); - return this.createJsonErrorResponse(403, -32000, error); - } - } - - // Validate Origin header if allowedOrigins is configured - if (this._allowedOrigins && this._allowedOrigins.length > 0) { - const originHeader = req.headers.get('origin'); - if (originHeader && !this._allowedOrigins.includes(originHeader)) { - const error = `Invalid Origin header: ${originHeader}`; - this.onerror?.(new Error(error)); - return this.createJsonErrorResponse(403, -32000, error); - } - } - - return undefined; - } - - /** - * Handles an incoming HTTP request, whether GET, POST, or DELETE - * Returns a Response object (Web Standard) - */ - async handleRequest(req: Request, options?: HandleRequestOptions): Promise { - // Validate request headers for DNS rebinding protection - const validationError = this.validateRequestHeaders(req); - if (validationError) { - return validationError; - } - - switch (req.method) { - case 'POST': - return this.handlePostRequest(req, options); - case 'GET': - return this.handleGetRequest(req); - case 'DELETE': - return this.handleDeleteRequest(req); - default: - return this.handleUnsupportedRequest(); - } - } - - /** - * Writes a priming event to establish resumption capability. - * Only sends if eventStore is configured (opt-in for resumability) and - * the client's protocol version supports empty SSE data (>= 2025-11-25). - */ - private async writePrimingEvent( - controller: ReadableStreamDefaultController, - encoder: TextEncoder, - streamId: string, - protocolVersion: string - ): Promise { - if (!this._eventStore) { - return; - } - - // Priming events have empty data which older clients cannot handle. - // Only send priming events to clients with protocol version >= 2025-11-25 - // which includes the fix for handling empty SSE data. - if (protocolVersion < '2025-11-25') { - return; - } - - const primingEventId = await this._eventStore.storeEvent(streamId, {} as JSONRPCMessage); - - let primingEvent = `id: ${primingEventId}\ndata: \n\n`; - if (this._retryInterval !== undefined) { - primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`; - } - controller.enqueue(encoder.encode(primingEvent)); - } - - /** - * Handles GET requests for SSE stream - */ - private async handleGetRequest(req: Request): Promise { - // The client MUST include an Accept header, listing text/event-stream as a supported content type. - const acceptHeader = req.headers.get('accept'); - if (!acceptHeader?.includes('text/event-stream')) { - return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept text/event-stream'); - } - - // If an Mcp-Session-Id is returned by the server during initialization, - // clients using the Streamable HTTP transport MUST include it - // in the Mcp-Session-Id header on all of their subsequent HTTP requests. - const sessionError = this.validateSession(req); - if (sessionError) { - return sessionError; - } - const protocolError = this.validateProtocolVersion(req); - if (protocolError) { - return protocolError; - } - - // Handle resumability: check for Last-Event-ID header - if (this._eventStore) { - const lastEventId = req.headers.get('last-event-id'); - if (lastEventId) { - return this.replayEvents(lastEventId); - } - } - - // Check if there's already an active standalone SSE stream for this session - if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) { - // Only one GET SSE stream is allowed per session - return this.createJsonErrorResponse(409, -32000, 'Conflict: Only one SSE stream is allowed per session'); - } - - const encoder = new TextEncoder(); - let streamController: ReadableStreamDefaultController; - - // Create a ReadableStream with a controller we can use to push SSE events - const readable = new ReadableStream({ - start: controller => { - streamController = controller; - }, - cancel: () => { - // Stream was cancelled by client - this._streamMapping.delete(this._standaloneSseStreamId); - } - }); - - const headers: Record = { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache, no-transform', - Connection: 'keep-alive' - }; - - // After initialization, always include the session ID if we have one - if (this.sessionId !== undefined) { - headers['mcp-session-id'] = this.sessionId; - } - - // Store the stream mapping with the controller for pushing data - this._streamMapping.set(this._standaloneSseStreamId, { - controller: streamController!, - encoder, - cleanup: () => { - this._streamMapping.delete(this._standaloneSseStreamId); - try { - streamController!.close(); - } catch { - // Controller might already be closed - } - } - }); - - return new Response(readable, { headers }); - } - - /** - * Replays events that would have been sent after the specified event ID - * Only used when resumability is enabled - */ - private async replayEvents(lastEventId: string): Promise { - if (!this._eventStore) { - return this.createJsonErrorResponse(400, -32000, 'Event store not configured'); - } - - try { - // If getStreamIdForEventId is available, use it for conflict checking - let streamId: string | undefined; - if (this._eventStore.getStreamIdForEventId) { - streamId = await this._eventStore.getStreamIdForEventId(lastEventId); - - if (!streamId) { - return this.createJsonErrorResponse(400, -32000, 'Invalid event ID format'); - } - - // Check conflict with the SAME streamId we'll use for mapping - if (this._streamMapping.get(streamId) !== undefined) { - return this.createJsonErrorResponse(409, -32000, 'Conflict: Stream already has an active connection'); - } - } - - const headers: Record = { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache, no-transform', - Connection: 'keep-alive' - }; - - if (this.sessionId !== undefined) { - headers['mcp-session-id'] = this.sessionId; - } - - // Create a ReadableStream with controller for SSE - const encoder = new TextEncoder(); - let streamController: ReadableStreamDefaultController; - - const readable = new ReadableStream({ - start: controller => { - streamController = controller; - }, - cancel: () => { - // Stream was cancelled by client - // Cleanup will be handled by the mapping - } - }); - - // Replay events - returns the streamId for backwards compatibility - const replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, { - send: async (eventId: string, message: JSONRPCMessage) => { - const success = this.writeSSEEvent(streamController!, encoder, message, eventId); - if (!success) { - this.onerror?.(new Error('Failed replay events')); - try { - streamController!.close(); - } catch { - // Controller might already be closed - } - } - } - }); - - this._streamMapping.set(replayedStreamId, { - controller: streamController!, - encoder, - cleanup: () => { - this._streamMapping.delete(replayedStreamId); - try { - streamController!.close(); - } catch { - // Controller might already be closed - } - } - }); - - return new Response(readable, { headers }); - } catch (error) { - this.onerror?.(error as Error); - return this.createJsonErrorResponse(500, -32000, 'Error replaying events'); - } - } - - /** - * Writes an event to an SSE stream via controller with proper formatting - */ - private writeSSEEvent( - controller: ReadableStreamDefaultController, - encoder: TextEncoder, - message: JSONRPCMessage, - eventId?: string - ): boolean { - try { - let eventData = `event: message\n`; - // Include event ID if provided - this is important for resumability - if (eventId) { - eventData += `id: ${eventId}\n`; - } - eventData += `data: ${JSON.stringify(message)}\n\n`; - controller.enqueue(encoder.encode(eventData)); - return true; - } catch { - return false; - } - } - - /** - * Handles unsupported requests (PUT, PATCH, etc.) - */ - private handleUnsupportedRequest(): Response { - return new Response( - JSON.stringify({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Method not allowed.' - }, - id: null - }), - { - status: 405, - headers: { - Allow: 'GET, POST, DELETE', - 'Content-Type': 'application/json' - } - } - ); - } - - /** - * Handles POST requests containing JSON-RPC messages - */ - private async handlePostRequest(req: Request, options?: HandleRequestOptions): Promise { - try { - // Validate the Accept header - const acceptHeader = req.headers.get('accept'); - // The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types. - if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) { - return this.createJsonErrorResponse( - 406, - -32000, - 'Not Acceptable: Client must accept both application/json and text/event-stream' - ); - } - - const ct = req.headers.get('content-type'); - if (!ct || !ct.includes('application/json')) { - return this.createJsonErrorResponse(415, -32000, 'Unsupported Media Type: Content-Type must be application/json'); - } - - // Build request info from headers - const requestInfo: RequestInfo = { - headers: Object.fromEntries(req.headers.entries()) - }; - - let rawMessage; - if (options?.parsedBody !== undefined) { - rawMessage = options.parsedBody; - } else { - try { - rawMessage = await req.json(); - } catch { - return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON'); - } - } - - let messages: JSONRPCMessage[]; - - // handle batch and single messages - try { - if (Array.isArray(rawMessage)) { - messages = rawMessage.map(msg => JSONRPCMessageSchema.parse(msg)); - } else { - messages = [JSONRPCMessageSchema.parse(rawMessage)]; - } - } catch { - return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON-RPC message'); - } - - // Check if this is an initialization request - // https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle/ - const isInitializationRequest = messages.some(isInitializeRequest); - if (isInitializationRequest) { - // If it's a server with session management and the session ID is already set we should reject the request - // to avoid re-initialization. - if (this._initialized && this.sessionId !== undefined) { - return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Server already initialized'); - } - if (messages.length > 1) { - return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Only one initialization request is allowed'); - } - this.sessionId = this.sessionIdGenerator?.(); - this._initialized = true; - - // If we have a session ID and an onsessioninitialized handler, call it immediately - // This is needed in cases where the server needs to keep track of multiple sessions - if (this.sessionId && this._onsessioninitialized) { - await Promise.resolve(this._onsessioninitialized(this.sessionId)); - } - } - if (!isInitializationRequest) { - // If an Mcp-Session-Id is returned by the server during initialization, - // clients using the Streamable HTTP transport MUST include it - // in the Mcp-Session-Id header on all of their subsequent HTTP requests. - const sessionError = this.validateSession(req); - if (sessionError) { - return sessionError; - } - // Mcp-Protocol-Version header is required for all requests after initialization. - const protocolError = this.validateProtocolVersion(req); - if (protocolError) { - return protocolError; - } - } - - // check if it contains requests - const hasRequests = messages.some(isJSONRPCRequest); - - if (!hasRequests) { - // if it only contains notifications or responses, return 202 - for (const message of messages) { - this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo }); - } - return new Response(null, { - status: 202, - headers: { - 'Content-Type': 'application/json' - } - }); - } - - // The default behavior is to use SSE streaming - // but in some cases server will return JSON responses - const streamId = crypto.randomUUID(); - - // Extract protocol version for priming event decision. - // For initialize requests, get from request params. - // For other requests, get from header (already validated). - const initRequest = messages.find(m => isInitializeRequest(m)); - const clientProtocolVersion = initRequest - ? initRequest.params.protocolVersion - : (req.headers.get('mcp-protocol-version') ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION); - - if (this._enableJsonResponse) { - // For JSON response mode, return a Promise that resolves when all responses are ready - return new Promise(resolve => { - this._streamMapping.set(streamId, { - resolveJson: resolve, - cleanup: () => { - this._streamMapping.delete(streamId); - } - }); - - for (const message of messages) { - if (isJSONRPCRequest(message)) { - this._requestToStreamMapping.set(message.id, streamId); - } - } - - for (const message of messages) { - this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo }); - } - }); - } - - // SSE streaming mode - use ReadableStream with controller for more reliable data pushing - const encoder = new TextEncoder(); - let streamController: ReadableStreamDefaultController; - - const readable = new ReadableStream({ - start: controller => { - streamController = controller; - }, - cancel: () => { - // Stream was cancelled by client - this._streamMapping.delete(streamId); - } - }); - - const headers: Record = { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive' - }; - - // After initialization, always include the session ID if we have one - if (this.sessionId !== undefined) { - headers['mcp-session-id'] = this.sessionId; - } - - // Store the response for this request to send messages back through this connection - // We need to track by request ID to maintain the connection - for (const message of messages) { - if (isJSONRPCRequest(message)) { - this._streamMapping.set(streamId, { - controller: streamController!, - encoder, - cleanup: () => { - this._streamMapping.delete(streamId); - try { - streamController!.close(); - } catch { - // Controller might already be closed - } - } - }); - this._requestToStreamMapping.set(message.id, streamId); - } - } - - // Write priming event if event store is configured (after mapping is set up) - await this.writePrimingEvent(streamController!, encoder, streamId, clientProtocolVersion); - - // handle each message - for (const message of messages) { - // Build closeSSEStream callback for requests when eventStore is configured - // AND client supports resumability (protocol version >= 2025-11-25). - // Old clients can't resume if the stream is closed early because they - // didn't receive a priming event with an event ID. - let closeSSEStream: (() => void) | undefined; - let closeStandaloneSSEStream: (() => void) | undefined; - if (isJSONRPCRequest(message) && this._eventStore && clientProtocolVersion >= '2025-11-25') { - closeSSEStream = () => { - this.closeSSEStream(message.id); - }; - closeStandaloneSSEStream = () => { - this.closeStandaloneSSEStream(); - }; - } - - this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo, closeSSEStream, closeStandaloneSSEStream }); - } - // The server SHOULD NOT close the SSE stream before sending all JSON-RPC responses - // This will be handled by the send() method when responses are ready - - return new Response(readable, { status: 200, headers }); - } catch (error) { - // return JSON-RPC formatted error - this.onerror?.(error as Error); - return this.createJsonErrorResponse(400, -32700, 'Parse error', { data: String(error) }); - } - } - - /** - * Handles DELETE requests to terminate sessions - */ - private async handleDeleteRequest(req: Request): Promise { - const sessionError = this.validateSession(req); - if (sessionError) { - return sessionError; - } - const protocolError = this.validateProtocolVersion(req); - if (protocolError) { - return protocolError; - } - - await Promise.resolve(this._onsessionclosed?.(this.sessionId!)); - await this.close(); - return new Response(null, { status: 200 }); - } - - /** - * Validates session ID for non-initialization requests. - * Returns Response error if invalid, undefined otherwise - */ - private validateSession(req: Request): Response | undefined { - if (this.sessionIdGenerator === undefined) { - // If the sessionIdGenerator ID is not set, the session management is disabled - // and we don't need to validate the session ID - return undefined; - } - if (!this._initialized) { - // If the server has not been initialized yet, reject all requests - return this.createJsonErrorResponse(400, -32000, 'Bad Request: Server not initialized'); - } - - const sessionId = req.headers.get('mcp-session-id'); - - if (!sessionId) { - // Non-initialization requests without a session ID should return 400 Bad Request - return this.createJsonErrorResponse(400, -32000, 'Bad Request: Mcp-Session-Id header is required'); - } - - if (sessionId !== this.sessionId) { - // Reject requests with invalid session ID with 404 Not Found - return this.createJsonErrorResponse(404, -32001, 'Session not found'); - } - - return undefined; - } - - /** - * Validates the MCP-Protocol-Version header on incoming requests. - * - * For initialization: Version negotiation handles unknown versions gracefully - * (server responds with its supported version). - * - * For subsequent requests with MCP-Protocol-Version header: - * - Accept if in supported list - * - 400 if unsupported - * - * For HTTP requests without the MCP-Protocol-Version header: - * - Accept and default to the version negotiated at initialization - */ - private validateProtocolVersion(req: Request): Response | undefined { - const protocolVersion = req.headers.get('mcp-protocol-version'); - - if (protocolVersion !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) { - return this.createJsonErrorResponse( - 400, - -32000, - `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})` - ); - } - return undefined; - } - - async close(): Promise { - // Close all SSE connections - this._streamMapping.forEach(({ cleanup }) => { - cleanup(); - }); - this._streamMapping.clear(); - - // Clear any pending responses - this._requestResponseMap.clear(); - this.onclose?.(); - } - - /** - * Close an SSE stream for a specific request, triggering client reconnection. - * Use this to implement polling behavior during long-running operations - - * client will reconnect after the retry interval specified in the priming event. - */ - closeSSEStream(requestId: RequestId): void { - const streamId = this._requestToStreamMapping.get(requestId); - if (!streamId) return; - - const stream = this._streamMapping.get(streamId); - if (stream) { - stream.cleanup(); - } - } - - /** - * Close the standalone GET SSE stream, triggering client reconnection. - * Use this to implement polling behavior for server-initiated notifications. - */ - closeStandaloneSSEStream(): void { - const stream = this._streamMapping.get(this._standaloneSseStreamId); - if (stream) { - stream.cleanup(); - } - } - - async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise { - let requestId = options?.relatedRequestId; - if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { - // If the message is a response, use the request ID from the message - requestId = message.id; - } - - // Check if this message should be sent on the standalone SSE stream (no request ID) - // Ignore notifications from tools (which have relatedRequestId set) - // Those will be sent via dedicated response SSE streams - if (requestId === undefined) { - // For standalone SSE streams, we can only send requests and notifications - if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { - throw new Error('Cannot send a response on a standalone SSE stream unless resuming a previous client request'); - } - - // Generate and store event ID if event store is provided - // Store even if stream is disconnected so events can be replayed on reconnect - let eventId: string | undefined; - if (this._eventStore) { - // Stores the event and gets the generated event ID - eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message); - } - - const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId); - if (standaloneSse === undefined) { - // Stream is disconnected - event is stored for replay, nothing more to do - return; - } - - // Send the message to the standalone SSE stream - if (standaloneSse.controller && standaloneSse.encoder) { - this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId); - } - return; - } - - // Get the response for this request - const streamId = this._requestToStreamMapping.get(requestId); - if (!streamId) { - throw new Error(`No connection established for request ID: ${String(requestId)}`); - } - - const stream = this._streamMapping.get(streamId); - - if (!this._enableJsonResponse && stream?.controller && stream?.encoder) { - // For SSE responses, generate event ID if event store is provided - let eventId: string | undefined; - - if (this._eventStore) { - eventId = await this._eventStore.storeEvent(streamId, message); - } - // Write the event to the response stream - this.writeSSEEvent(stream.controller, stream.encoder, message, eventId); - } - - if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { - this._requestResponseMap.set(requestId, message); - const relatedIds = Array.from(this._requestToStreamMapping.entries()) - .filter(([_, sid]) => sid === streamId) - .map(([id]) => id); - - // Check if we have responses for all requests using this connection - const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id)); - - if (allResponsesReady) { - if (!stream) { - throw new Error(`No connection established for request ID: ${String(requestId)}`); - } - if (this._enableJsonResponse && stream.resolveJson) { - // All responses ready, send as JSON - const headers: Record = { - 'Content-Type': 'application/json' - }; - if (this.sessionId !== undefined) { - headers['mcp-session-id'] = this.sessionId; - } - - const responses = relatedIds.map(id => this._requestResponseMap.get(id)!); - - if (responses.length === 1) { - stream.resolveJson(new Response(JSON.stringify(responses[0]), { status: 200, headers })); - } else { - stream.resolveJson(new Response(JSON.stringify(responses), { status: 200, headers })); - } - } else { - // End the SSE stream - stream.cleanup(); - } - // Clean up - for (const id of relatedIds) { - this._requestResponseMap.delete(id); - this._requestToStreamMapping.delete(id); - } - } - } - } -} diff --git a/packages/server/test/server/__fixtures__/zodTestMatrix.ts b/packages/server/test/server/__fixtures__/zodTestMatrix.ts deleted file mode 100644 index fc4ee63db..000000000 --- a/packages/server/test/server/__fixtures__/zodTestMatrix.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as z3 from 'zod/v3'; -import * as z4 from 'zod/v4'; - -// Shared Zod namespace type that exposes the common surface area used in tests. -export type ZNamespace = typeof z3 & typeof z4; - -export const zodTestMatrix = [ - { - zodVersionLabel: 'Zod v3', - z: z3 as ZNamespace, - isV3: true as const, - isV4: false as const - }, - { - zodVersionLabel: 'Zod v4', - z: z4 as ZNamespace, - isV3: false as const, - isV4: true as const - } -] as const; - -export type ZodMatrixEntry = (typeof zodTestMatrix)[number]; diff --git a/packages/server/test/server/completable.test.ts b/packages/server/test/server/completable.test.ts index ff94b641b..9195ca623 100644 --- a/packages/server/test/server/completable.test.ts +++ b/packages/server/test/server/completable.test.ts @@ -1,6 +1,8 @@ +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { zodTestMatrix } from '@modelcontextprotocol/test-helpers'; +import { describe, expect, it } from 'vitest'; + import { completable, getCompleter } from '../../src/server/completable.js'; -import type { ZodMatrixEntry } from './__fixtures__/zodTestMatrix.js'; -import { zodTestMatrix } from './__fixtures__/zodTestMatrix.js'; describe.each(zodTestMatrix)('completable with $zodVersionLabel', (entry: ZodMatrixEntry) => { const { z } = entry; diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/server/test/server/streamableHttp.test.ts index 6dbd86044..be53a37f1 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/server/test/server/streamableHttp.test.ts @@ -1,62 +1,12 @@ import { randomUUID } from 'node:crypto'; -import type { IncomingMessage, Server, ServerResponse } from 'node:http'; -import { createServer } from 'node:http'; -import type { AddressInfo } from 'node:net'; -import { createServer as netCreateServer } from 'node:net'; - -import type { - AuthInfo, - CallToolResult, - JSONRPCErrorResponse, - JSONRPCMessage, - JSONRPCResultResponse, - RequestId -} from '@modelcontextprotocol/core'; -import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers'; -import { McpServer } from '../../src/server/mcp.js'; -import { NodeStreamableHTTPServerTransport } from '../../src/server/streamableHttp.js'; -import type { EventId, EventStore, StreamId } from '../../src/server/webStandardStreamableHttp.js'; -import type { ZodMatrixEntry } from './__fixtures__/zodTestMatrix.js'; -import { zodTestMatrix } from './__fixtures__/zodTestMatrix.js'; - -async function getFreePort() { - return new Promise(res => { - const srv = netCreateServer(); - srv.listen(0, () => { - const address = srv.address()!; - if (typeof address === 'string') { - throw new Error('Unexpected address type: ' + typeof address); - } - const port = (address as AddressInfo).port; - srv.close(_err => res(port)); - }); - }); -} - -/** - * Test server configuration for NodeStreamableHTTPServerTransport tests - */ -interface TestServerConfig { - sessionIdGenerator: (() => string) | undefined; - enableJsonResponse?: boolean; - customRequestHandler?: (req: IncomingMessage, res: ServerResponse, parsedBody?: unknown) => Promise; - eventStore?: EventStore; - onsessioninitialized?: (sessionId: string) => void | Promise; - onsessionclosed?: (sessionId: string) => void | Promise; - retryInterval?: number; -} +import type { CallToolResult, JSONRPCErrorResponse, JSONRPCMessage } from '@modelcontextprotocol/core'; +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { zodTestMatrix } from '@modelcontextprotocol/test-helpers'; -/** - * Helper to stop test server - */ -async function stopTestServer({ server, transport }: { server: Server; transport: NodeStreamableHTTPServerTransport }): Promise { - // First close the transport to ensure all SSE streams are closed - await transport.close(); - - // Close the server without waiting indefinitely - server.close(); -} +import { McpServer } from '../../src/server/mcp.js'; +import type { EventId, EventStore, StreamId } from '../../src/server/streamableHttp.js'; +import { WebStandardStreamableHTTPServerTransport } from '../../src/server/streamableHttp.js'; /** * Common test messages @@ -73,7 +23,6 @@ const TEST_MESSAGES = { id: 'init-1' } as JSONRPCMessage, - // Initialize message with an older protocol version for backward compatibility tests initializeOldVersion: { jsonrpc: '2.0', method: 'initialize', @@ -93,10 +42,53 @@ const TEST_MESSAGES = { } as JSONRPCMessage }; +/** + * Helper to create a Web Standard Request + */ +function createRequest( + method: string, + body?: JSONRPCMessage | JSONRPCMessage[], + options?: { + sessionId?: string; + accept?: string; + contentType?: string; + extraHeaders?: Record; + } +): Request { + const headers: Record = {}; + + if (options?.accept) { + headers['Accept'] = options.accept; + } else if (method === 'POST') { + headers['Accept'] = 'application/json, text/event-stream'; + } else if (method === 'GET') { + headers['Accept'] = 'text/event-stream'; + } + + if (options?.contentType) { + headers['Content-Type'] = options.contentType; + } else if (body) { + headers['Content-Type'] = 'application/json'; + } + + if (options?.sessionId) { + headers['mcp-session-id'] = options.sessionId; + headers['mcp-protocol-version'] = '2025-11-25'; + } + + if (options?.extraHeaders) { + Object.assign(headers, options.extraHeaders); + } + + return new Request('http://localhost/mcp', { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }); +} + /** * Helper to extract text from SSE response - * Note: Can only be called once per response stream. For multiple reads, - * get the reader manually and read multiple times. */ async function readSSEEvent(response: Response): Promise { const reader = response.body?.getReader(); @@ -105,38 +97,18 @@ async function readSSEEvent(response: Response): Promise { } /** - * Helper to send JSON-RPC request + * Helper to parse SSE data line */ -async function sendPostRequest( - baseUrl: URL, - message: JSONRPCMessage | JSONRPCMessage[], - sessionId?: string, - extraHeaders?: Record -): Promise { - const headers: Record = { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - ...extraHeaders - }; - - if (sessionId) { - headers['mcp-session-id'] = sessionId; - headers['mcp-protocol-version'] = '2025-11-25'; +function parseSSEData(text: string): unknown { + const eventLines = text.split('\n'); + const dataLine = eventLines.find(line => line.startsWith('data:')); + if (!dataLine) { + throw new Error('No data line found in SSE event'); } - - return fetch(baseUrl, { - method: 'POST', - headers, - body: JSON.stringify(message) - }); + return JSON.parse(dataLine.substring(5).trim()); } -function expectErrorResponse( - data: unknown, - expectedCode: number, - expectedMessagePattern: RegExp, - options?: { expectData?: boolean } -): void { +function expectErrorResponse(data: unknown, expectedCode: number, expectedMessagePattern: RegExp): void { expect(data).toMatchObject({ jsonrpc: '2.0', error: expect.objectContaining({ @@ -144,131 +116,42 @@ function expectErrorResponse( message: expect.stringMatching(expectedMessagePattern) }) }); - if (options?.expectData) { - expect((data as { error: { data?: string } }).error.data).toBeDefined(); - } } -describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { - /** - * Helper to create and start test HTTP server with MCP setup - */ - async function createTestServer(config: TestServerConfig = { sessionIdGenerator: () => randomUUID() }): Promise<{ - server: Server; - transport: NodeStreamableHTTPServerTransport; - mcpServer: McpServer; - baseUrl: URL; - }> { - const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); - - mcpServer.tool( - 'greet', - 'A simple greeting tool', - { name: z.string().describe('Name to greet') }, - async ({ name }): Promise => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; - } - ); - - const transport = new NodeStreamableHTTPServerTransport({ - sessionIdGenerator: config.sessionIdGenerator, - enableJsonResponse: config.enableJsonResponse ?? false, - eventStore: config.eventStore, - onsessioninitialized: config.onsessioninitialized, - onsessionclosed: config.onsessionclosed, - retryInterval: config.retryInterval - }); - - await mcpServer.connect(transport); - - const server = createServer(async (req, res) => { - try { - if (config.customRequestHandler) { - await config.customRequestHandler(req, res); - } else { - await transport.handleRequest(req, res); - } - } catch (error) { - console.error('Error handling request:', error); - if (!res.headersSent) res.writeHead(500).end(); - } - }); - - const baseUrl = await listenOnRandomPort(server); - return { server, transport, mcpServer, baseUrl }; - } +describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { + const { z } = entry; - /** - * Helper to create and start authenticated test HTTP server with MCP setup - */ - async function createTestAuthServer(config: TestServerConfig = { sessionIdGenerator: () => randomUUID() }): Promise<{ - server: Server; - transport: NodeStreamableHTTPServerTransport; - mcpServer: McpServer; - baseUrl: URL; - }> { - const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); - - mcpServer.tool( - 'profile', - 'A user profile data tool', - { active: z.boolean().describe('Profile status') }, - async ({ active }, { authInfo }): Promise => { - return { content: [{ type: 'text', text: `${active ? 'Active' : 'Inactive'} profile from token: ${authInfo?.token}!` }] }; - } - ); - - const transport = new NodeStreamableHTTPServerTransport({ - sessionIdGenerator: config.sessionIdGenerator, - enableJsonResponse: config.enableJsonResponse ?? false, - eventStore: config.eventStore, - onsessioninitialized: config.onsessioninitialized, - onsessionclosed: config.onsessionclosed - }); + describe('HTTPServerTransport', () => { + let transport: WebStandardStreamableHTTPServerTransport; + let mcpServer: McpServer; + let sessionId: string; - await mcpServer.connect(transport); + beforeEach(async () => { + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); - const server = createServer(async (req: IncomingMessage & { auth?: AuthInfo }, res) => { - try { - if (config.customRequestHandler) { - await config.customRequestHandler(req, res); - } else { - req.auth = { token: req.headers['authorization']?.split(' ')[1] } as AuthInfo; - await transport.handleRequest(req, res); + mcpServer.tool( + 'greet', + 'A simple greeting tool', + { name: z.string().describe('Name to greet') }, + async ({ name }): Promise => { + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; } - } catch (error) { - console.error('Error handling request:', error); - if (!res.headersSent) res.writeHead(500).end(); - } - }); - - const baseUrl = await listenOnRandomPort(server); - - return { server, transport, mcpServer, baseUrl }; - } + ); - const { z } = entry; - describe('NodeStreamableHTTPServerTransport', () => { - let server: Server; - let mcpServer: McpServer; - let transport: NodeStreamableHTTPServerTransport; - let baseUrl: URL; - let sessionId: string; + transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID() + }); - beforeEach(async () => { - const result = await createTestServer(); - server = result.server; - transport = result.transport; - mcpServer = result.mcpServer; - baseUrl = result.baseUrl; + await mcpServer.connect(transport); }); afterEach(async () => { - await stopTestServer({ server, transport }); + await transport.close(); }); async function initializeServer(): Promise { - const response = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); expect(response.status).toBe(200); const newSessionId = response.headers.get('mcp-session-id'); @@ -276,730 +159,513 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { return newSessionId as string; } - it('should initialize server and generate session ID', async () => { - const response = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - - expect(response.status).toBe(200); - expect(response.headers.get('content-type')).toBe('text/event-stream'); - expect(response.headers.get('mcp-session-id')).toBeDefined(); - }); + describe('Initialization', () => { + it('should initialize server and generate session ID', async () => { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); - it('should reject second initialization request', async () => { - // First initialize - const sessionId = await initializeServer(); - expect(sessionId).toBeDefined(); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + expect(response.headers.get('mcp-session-id')).toBeDefined(); + }); - // Try second initialize - const secondInitMessage = { - ...TEST_MESSAGES.initialize, - id: 'second-init' - }; + it('should reject second initialization request', async () => { + sessionId = await initializeServer(); + expect(sessionId).toBeDefined(); - const response = await sendPostRequest(baseUrl, secondInitMessage); + const secondInitMessage = { + ...TEST_MESSAGES.initialize, + id: 'second-init' + }; - expect(response.status).toBe(400); - const errorData = await response.json(); - expectErrorResponse(errorData, -32600, /Server already initialized/); - }); + const request = createRequest('POST', secondInitMessage); + const response = await transport.handleRequest(request); - it('should reject batch initialize request', async () => { - const batchInitMessages: JSONRPCMessage[] = [ - TEST_MESSAGES.initialize, - { - jsonrpc: '2.0', - method: 'initialize', - params: { - clientInfo: { name: 'test-client-2', version: '1.0' }, - protocolVersion: '2025-03-26' - }, - id: 'init-2' - } - ]; + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32600, /Server already initialized/); + }); + + it('should reject batch initialize request', async () => { + const batchInitMessages: JSONRPCMessage[] = [ + TEST_MESSAGES.initialize, + { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client-2', version: '1.0' }, + protocolVersion: '2025-03-26' + }, + id: 'init-2' + } + ]; - const response = await sendPostRequest(baseUrl, batchInitMessages); + const request = createRequest('POST', batchInitMessages); + const response = await transport.handleRequest(request); - expect(response.status).toBe(400); - const errorData = await response.json(); - expectErrorResponse(errorData, -32600, /Only one initialization request is allowed/); + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32600, /Only one initialization request is allowed/); + }); }); - it('should handle post requests via sse response correctly', async () => { - sessionId = await initializeServer(); - - const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId); + describe('POST Requests', () => { + it('should handle post requests via SSE response correctly', async () => { + sessionId = await initializeServer(); - expect(response.status).toBe(200); + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId }); + const response = await transport.handleRequest(request); - // Read the SSE stream for the response - const text = await readSSEEvent(response); + expect(response.status).toBe(200); - // Parse the SSE event - const eventLines = text.split('\n'); - const dataLine = eventLines.find(line => line.startsWith('data:')); - expect(dataLine).toBeDefined(); + const text = await readSSEEvent(response); + const eventData = parseSSEData(text); - const eventData = JSON.parse(dataLine!.substring(5)); - expect(eventData).toMatchObject({ - jsonrpc: '2.0', - result: expect.objectContaining({ - tools: expect.arrayContaining([ - expect.objectContaining({ - name: 'greet', - description: 'A simple greeting tool' - }) - ]) - }), - id: 'tools-1' + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: expect.objectContaining({ + tools: expect.arrayContaining([ + expect.objectContaining({ + name: 'greet', + description: 'A simple greeting tool' + }) + ]) + }), + id: 'tools-1' + }); }); - }); - - it('should call a tool and return the result', async () => { - sessionId = await initializeServer(); - const toolCallMessage: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'greet', - arguments: { - name: 'Test User' - } - }, - id: 'call-1' - }; + it('should call a tool and return the result', async () => { + sessionId = await initializeServer(); - const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId); - expect(response.status).toBe(200); + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'greet', + arguments: { + name: 'Test User' + } + }, + id: 'call-1' + }; - const text = await readSSEEvent(response); - const eventLines = text.split('\n'); - const dataLine = eventLines.find(line => line.startsWith('data:')); - expect(dataLine).toBeDefined(); + const request = createRequest('POST', toolCallMessage, { sessionId }); + const response = await transport.handleRequest(request); - const eventData = JSON.parse(dataLine!.substring(5)); - expect(eventData).toMatchObject({ - jsonrpc: '2.0', - result: { - content: [ - { - type: 'text', - text: 'Hello, Test User!' - } - ] - }, - id: 'call-1' - }); - }); + expect(response.status).toBe(200); - /*** - * Test: Tool With Request Info - */ - it('should pass request info to tool callback', async () => { - sessionId = await initializeServer(); + const text = await readSSEEvent(response); + const eventData = parseSSEData(text); - mcpServer.tool( - 'test-request-info', - 'A simple test tool with request info', - { name: z.string().describe('Name to greet') }, - async ({ name }, { requestInfo }): Promise => { - return { + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: { content: [ - { type: 'text', text: `Hello, ${name}!` }, - { type: 'text', text: `${JSON.stringify(requestInfo)}` } + { + type: 'text', + text: 'Hello, Test User!' + } ] - }; - } - ); - - const toolCallMessage: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'test-request-info', - arguments: { - name: 'Test User' - } - }, - id: 'call-1' - }; - - const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId); - expect(response.status).toBe(200); - - const text = await readSSEEvent(response); - const eventLines = text.split('\n'); - const dataLine = eventLines.find(line => line.startsWith('data:')); - expect(dataLine).toBeDefined(); - - const eventData = JSON.parse(dataLine!.substring(5)); - - expect(eventData).toMatchObject({ - jsonrpc: '2.0', - result: { - content: [ - { type: 'text', text: 'Hello, Test User!' }, - { type: 'text', text: expect.any(String) } - ] - }, - id: 'call-1' + }, + id: 'call-1' + }); }); - const requestInfo = JSON.parse(eventData.result.content[1].text); - expect(requestInfo).toMatchObject({ - headers: { - 'content-type': 'application/json', - accept: 'application/json, text/event-stream', - connection: 'keep-alive', - 'mcp-session-id': sessionId, - 'accept-language': '*', - 'user-agent': expect.any(String), - 'accept-encoding': expect.any(String), - 'content-length': expect.any(String) - } + it('should reject requests without a valid session ID', async () => { + const request = createRequest('POST', TEST_MESSAGES.toolsList); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = (await response.json()) as JSONRPCErrorResponse; + expectErrorResponse(errorData, -32000, /Bad Request/); + expect(errorData.id).toBeNull(); }); - }); - it('should reject requests without a valid session ID', async () => { - const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList); + it('should reject invalid session ID', async () => { + await initializeServer(); - expect(response.status).toBe(400); - const errorData = (await response.json()) as JSONRPCErrorResponse; - expectErrorResponse(errorData, -32000, /Bad Request/); - expect(errorData.id).toBeNull(); - }); + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId: 'invalid-session-id' }); + const response = await transport.handleRequest(request); - it('should reject invalid session ID', async () => { - // First initialize to be in valid state - await initializeServer(); + expect(response.status).toBe(404); + const errorData = await response.json(); + expectErrorResponse(errorData, -32001, /Session not found/); + }); - // Now try with invalid session ID - const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, 'invalid-session-id'); + it('should reject request with wrong Accept header', async () => { + const request = createRequest('POST', TEST_MESSAGES.initialize, { accept: 'application/json' }); + const response = await transport.handleRequest(request); - expect(response.status).toBe(404); - const errorData = await response.json(); - expectErrorResponse(errorData, -32001, /Session not found/); - }); + expect(response.status).toBe(406); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Not Acceptable/); + }); - it('should establish standalone SSE stream and receive server-initiated messages', async () => { - // First initialize to get a session ID - sessionId = await initializeServer(); + it('should reject request with wrong Content-Type header', async () => { + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'text/plain' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + const response = await transport.handleRequest(request); - // Open a standalone SSE stream - const sseResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } + expect(response.status).toBe(415); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Unsupported Media Type/); }); - expect(sseResponse.status).toBe(200); - expect(sseResponse.headers.get('content-type')).toBe('text/event-stream'); + it('should reject invalid JSON', async () => { + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json' + }, + body: 'not valid json' + }); + const response = await transport.handleRequest(request); - // Send a notification (server-initiated message) that should appear on SSE stream - const notification: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'notifications/message', - params: { level: 'info', data: 'Test notification' } - }; + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32700, /Parse error.*Invalid JSON/); + }); - // Send the notification via transport - await transport.send(notification); + it('should accept notifications without session and return 202', async () => { + sessionId = await initializeServer(); - // Read from the stream and verify we got the notification - const text = await readSSEEvent(sseResponse); + const notification: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'notifications/initialized' + }; - const eventLines = text.split('\n'); - const dataLine = eventLines.find(line => line.startsWith('data:')); - expect(dataLine).toBeDefined(); + const request = createRequest('POST', notification, { sessionId }); + const response = await transport.handleRequest(request); - const eventData = JSON.parse(dataLine!.substring(5)); - expect(eventData).toMatchObject({ - jsonrpc: '2.0', - method: 'notifications/message', - params: { level: 'info', data: 'Test notification' } + expect(response.status).toBe(202); }); }); - it('should not close GET SSE stream after sending multiple server notifications', async () => { - sessionId = await initializeServer(); + describe('GET Requests (SSE Stream)', () => { + it('should establish standalone SSE stream', async () => { + sessionId = await initializeServer(); - // Open a standalone SSE stream - const sseResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } + const request = createRequest('GET', undefined, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + expect(response.headers.get('mcp-session-id')).toBe(sessionId); }); - expect(sseResponse.status).toBe(200); - const reader = sseResponse.body?.getReader(); + it('should reject GET without Accept: text/event-stream', async () => { + sessionId = await initializeServer(); - // Send multiple notifications - const notification1: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'notifications/message', - params: { level: 'info', data: 'First notification' } - }; + const request = createRequest('GET', undefined, { sessionId, accept: 'application/json' }); + const response = await transport.handleRequest(request); - // Just send one and verify it comes through - then the stream should stay open - await transport.send(notification1); + expect(response.status).toBe(406); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Not Acceptable/); + }); - const { value, done } = await reader!.read(); - const text = new TextDecoder().decode(value); - expect(text).toContain('First notification'); - expect(done).toBe(false); // Stream should still be open - }); + it('should reject second standalone SSE stream', async () => { + sessionId = await initializeServer(); - it('should reject second SSE stream for the same session', async () => { - sessionId = await initializeServer(); + // First SSE stream + const request1 = createRequest('GET', undefined, { sessionId }); + const response1 = await transport.handleRequest(request1); + expect(response1.status).toBe(200); - // Open first SSE stream - const firstStream = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } + // Second SSE stream should be rejected + const request2 = createRequest('GET', undefined, { sessionId }); + const response2 = await transport.handleRequest(request2); + + expect(response2.status).toBe(409); + const errorData = await response2.json(); + expectErrorResponse(errorData, -32000, /Conflict/); }); + }); - expect(firstStream.status).toBe(200); + describe('DELETE Requests', () => { + it('should handle DELETE to close session', async () => { + sessionId = await initializeServer(); - // Try to open a second SSE stream with the same session ID - const secondStream = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } + const request = createRequest('DELETE', undefined, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); }); - // Should be rejected - expect(secondStream.status).toBe(409); // Conflict - const errorData = await secondStream.json(); - expectErrorResponse(errorData, -32000, /Only one SSE stream is allowed per session/); - }); + it('should reject DELETE without valid session', async () => { + await initializeServer(); - it('should reject GET requests without Accept: text/event-stream header', async () => { - sessionId = await initializeServer(); + const request = createRequest('DELETE', undefined, { sessionId: 'invalid-session' }); + const response = await transport.handleRequest(request); - // Try GET without proper Accept header - const response = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } + expect(response.status).toBe(404); }); - - expect(response.status).toBe(406); - const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Client must accept text\/event-stream/); }); - it('should reject POST requests without proper Accept header', async () => { - sessionId = await initializeServer(); + describe('Unsupported Methods', () => { + it('should reject PUT requests', async () => { + const request = new Request('http://localhost/mcp', { method: 'PUT' }); + const response = await transport.handleRequest(request); - // Try POST without Accept: text/event-stream - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', // Missing text/event-stream - 'mcp-session-id': sessionId - }, - body: JSON.stringify(TEST_MESSAGES.toolsList) + expect(response.status).toBe(405); + expect(response.headers.get('Allow')).toBe('GET, POST, DELETE'); }); - expect(response.status).toBe(406); - const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Client must accept both application\/json and text\/event-stream/); + it('should reject PATCH requests', async () => { + const request = new Request('http://localhost/mcp', { method: 'PATCH' }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(405); + }); }); + }); - it('should reject unsupported Content-Type', async () => { - sessionId = await initializeServer(); + describe('HTTPServerTransport - Stateless Mode', () => { + let transport: WebStandardStreamableHTTPServerTransport; + let mcpServer: McpServer; - // Try POST with text/plain Content-Type - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId - }, - body: 'This is plain text' + beforeEach(async () => { + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + + mcpServer.tool('echo', 'Echo tool', { message: z.string() }, async ({ message }): Promise => { + return { content: [{ type: 'text', text: message }] }; }); - expect(response.status).toBe(415); - const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Content-Type must be application\/json/); + transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: undefined + }); + + await mcpServer.connect(transport); }); - it('should handle JSON-RPC batch notification messages with 202 response', async () => { - sessionId = await initializeServer(); + afterEach(async () => { + await transport.close(); + }); - // Send batch of notifications (no IDs) - const batchNotifications: JSONRPCMessage[] = [ - { jsonrpc: '2.0', method: 'someNotification1', params: {} }, - { jsonrpc: '2.0', method: 'someNotification2', params: {} } - ]; - const response = await sendPostRequest(baseUrl, batchNotifications, sessionId); + it('should work without session management', async () => { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); - expect(response.status).toBe(202); - expect(response.headers.get('content-type')).toBe('application/json'); + expect(response.status).toBe(200); + expect(response.headers.get('mcp-session-id')).toBeNull(); }); - it('should handle batch request messages with SSE stream for responses', async () => { - sessionId = await initializeServer(); + it('should not require session ID on subsequent requests', async () => { + // Initialize + const initRequest = createRequest('POST', TEST_MESSAGES.initialize); + await transport.handleRequest(initRequest); - // Send batch of requests - const batchRequests: JSONRPCMessage[] = [ - { jsonrpc: '2.0', method: 'tools/list', params: {}, id: 'req-1' }, - { jsonrpc: '2.0', method: 'tools/call', params: { name: 'greet', arguments: { name: 'BatchUser' } }, id: 'req-2' } - ]; - const response = await sendPostRequest(baseUrl, batchRequests, sessionId); + // Subsequent request without session ID should work + const request = createRequest('POST', TEST_MESSAGES.toolsList); + const response = await transport.handleRequest(request); expect(response.status).toBe(200); - expect(response.headers.get('content-type')).toBe('text/event-stream'); + }); + }); - const reader = response.body?.getReader(); + describe('HTTPServerTransport - JSON Response Mode', () => { + let transport: WebStandardStreamableHTTPServerTransport; + let mcpServer: McpServer; + let sessionId: string; - // The responses may come in any order or together in one chunk - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); + beforeEach(async () => { + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); - // Check that both responses were sent on the same stream - expect(text).toContain('"id":"req-1"'); - expect(text).toContain('"tools"'); // tools/list result - expect(text).toContain('"id":"req-2"'); - expect(text).toContain('Hello, BatchUser'); // tools/call result - }); - - it('should properly handle invalid JSON data', async () => { - sessionId = await initializeServer(); + mcpServer.tool('greet', 'Greeting tool', { name: z.string() }, async ({ name }): Promise => { + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + }); - // Send invalid JSON - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId - }, - body: 'This is not valid JSON' + transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + enableJsonResponse: true }); - expect(response.status).toBe(400); - const errorData = await response.json(); - expectErrorResponse(errorData, -32700, /Parse error/); + await mcpServer.connect(transport); }); - it('should include error data in parse error response for unexpected errors', async () => { - sessionId = await initializeServer(); + afterEach(async () => { + await transport.close(); + }); - // We can't easily trigger the catch-all error handler, but we can verify - // that the JSON parse error includes useful information - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId - }, - body: '{ invalid json }' - }); + async function initializeServer(): Promise { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); - expect(response.status).toBe(400); - const errorData = (await response.json()) as JSONRPCErrorResponse; - expectErrorResponse(errorData, -32700, /Parse error/); - // The error message should contain details about what went wrong - expect(errorData.error.message).toContain('Invalid JSON'); - }); + expect(response.status).toBe(200); + const newSessionId = response.headers.get('mcp-session-id'); + expect(newSessionId).toBeDefined(); + return newSessionId as string; + } - it('should return 400 error for invalid JSON-RPC messages', async () => { + it('should return JSON response instead of SSE', async () => { sessionId = await initializeServer(); - // Invalid JSON-RPC (missing required jsonrpc version) - const invalidMessage = { method: 'tools/list', params: {}, id: 1 }; // missing jsonrpc version - const response = await sendPostRequest(baseUrl, invalidMessage as JSONRPCMessage, sessionId); - - expect(response.status).toBe(400); - const errorData = await response.json(); - expect(errorData).toMatchObject({ - jsonrpc: '2.0', - error: expect.anything() - }); - }); + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId }); + const response = await transport.handleRequest(request); - it('should reject requests to uninitialized server', async () => { - // Create a new HTTP server and transport without initializing - const { server: uninitializedServer, transport: uninitializedTransport, baseUrl: uninitializedUrl } = await createTestServer(); - // Transport not used in test but needed for cleanup + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); - // No initialization, just send a request directly - const uninitializedMessage: JSONRPCMessage = { + const data = await response.json(); + expect(data).toMatchObject({ jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 'uninitialized-test' - }; - - // Send a request to uninitialized server - const response = await sendPostRequest(uninitializedUrl, uninitializedMessage, 'any-session-id'); - - expect(response.status).toBe(400); - const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Server not initialized/); - - // Cleanup - await stopTestServer({ server: uninitializedServer, transport: uninitializedTransport }); + result: expect.objectContaining({ + tools: expect.any(Array) + }), + id: 'tools-1' + }); }); - it('should send response messages to the connection that sent the request', async () => { + it('should handle tool calls in JSON response mode', async () => { sessionId = await initializeServer(); - const message1: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 'req-1' - }; - - const message2: JSONRPCMessage = { + const toolCallMessage: JSONRPCMessage = { jsonrpc: '2.0', method: 'tools/call', params: { name: 'greet', - arguments: { name: 'Connection2' } + arguments: { name: 'World' } }, - id: 'req-2' + id: 'call-1' }; - // Make two concurrent fetch connections for different requests - const req1 = sendPostRequest(baseUrl, message1, sessionId); - const req2 = sendPostRequest(baseUrl, message2, sessionId); - - // Get both responses - const [response1, response2] = await Promise.all([req1, req2]); - const reader1 = response1.body?.getReader(); - const reader2 = response2.body?.getReader(); - - // Read responses from each stream (requires each receives its specific response) - const { value: value1 } = await reader1!.read(); - const text1 = new TextDecoder().decode(value1); - expect(text1).toContain('"id":"req-1"'); - expect(text1).toContain('"tools"'); // tools/list result - - const { value: value2 } = await reader2!.read(); - const text2 = new TextDecoder().decode(value2); - expect(text2).toContain('"id":"req-2"'); - expect(text2).toContain('Hello, Connection2'); // tools/call result - }); - - it('should keep stream open after sending server notifications', async () => { - sessionId = await initializeServer(); - - // Open a standalone SSE stream - const sseResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } - }); + const request = createRequest('POST', toolCallMessage, { sessionId }); + const response = await transport.handleRequest(request); - // Send several server-initiated notifications - await transport.send({ - jsonrpc: '2.0', - method: 'notifications/message', - params: { level: 'info', data: 'First notification' } - }); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); - await transport.send({ + const data = await response.json(); + expect(data).toMatchObject({ jsonrpc: '2.0', - method: 'notifications/message', - params: { level: 'info', data: 'Second notification' } + result: { + content: [{ type: 'text', text: 'Hello, World!' }] + }, + id: 'call-1' }); - - // Stream should still be open - it should not close after sending notifications - expect(sseResponse.bodyUsed).toBe(false); }); + }); - // The current implementation will close the entire transport for DELETE - // Creating a temporary transport/server where we don't care if it gets closed - it('should properly handle DELETE requests and close session', async () => { - // Setup a temporary server for this test - const tempResult = await createTestServer(); - const tempServer = tempResult.server; - const tempUrl = tempResult.baseUrl; - - // Initialize to get a session ID - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - const tempSessionId = initResponse.headers.get('mcp-session-id'); - - // Now DELETE the session - const deleteResponse = await fetch(tempUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-11-25' - } - }); + describe('HTTPServerTransport - Session Callbacks', () => { + it('should call onsessioninitialized callback', async () => { + const onInitialized = vi.fn(); - expect(deleteResponse.status).toBe(200); + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: {} }); + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => 'test-session-123', + onsessioninitialized: onInitialized + }); - // Clean up - don't wait indefinitely for server close - tempServer.close(); - }); + await mcpServer.connect(transport); - it('should reject DELETE requests with invalid session ID', async () => { - // Initialize the server first to activate it - sessionId = await initializeServer(); + const request = createRequest('POST', TEST_MESSAGES.initialize); + await transport.handleRequest(request); - // Try to delete with invalid session ID - const response = await fetch(baseUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': 'invalid-session-id', - 'mcp-protocol-version': '2025-11-25' - } - }); + expect(onInitialized).toHaveBeenCalledWith('test-session-123'); - expect(response.status).toBe(404); - const errorData = await response.json(); - expectErrorResponse(errorData, -32001, /Session not found/); + await transport.close(); }); - describe('protocol version header validation', () => { - it('should accept requests with matching protocol version', async () => { - sessionId = await initializeServer(); - - // Send request with matching protocol version - const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId); + it('should call onsessionclosed callback on DELETE', async () => { + const onClosed = vi.fn(); - expect(response.status).toBe(200); + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: {} }); + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => 'test-session-456', + onsessionclosed: onClosed }); - it('should accept requests without protocol version header', async () => { - sessionId = await initializeServer(); - - // Send request without protocol version header - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId - // No mcp-protocol-version header - }, - body: JSON.stringify(TEST_MESSAGES.toolsList) - }); - - expect(response.status).toBe(200); - }); + await mcpServer.connect(transport); - it('should reject requests with unsupported protocol version', async () => { - sessionId = await initializeServer(); + // Initialize first + const initRequest = createRequest('POST', TEST_MESSAGES.initialize); + await transport.handleRequest(initRequest); - // Send request with unsupported protocol version - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '1999-01-01' // Unsupported version - }, - body: JSON.stringify(TEST_MESSAGES.toolsList) - }); + // Then delete + const deleteRequest = createRequest('DELETE', undefined, { sessionId: 'test-session-456' }); + await transport.handleRequest(deleteRequest); - expect(response.status).toBe(400); - const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version: .+ \(supported versions: .+\)/); - }); + expect(onClosed).toHaveBeenCalledWith('test-session-456'); + }); + }); - it('should accept when protocol version differs from negotiated version', async () => { - sessionId = await initializeServer(); + describe('HTTPServerTransport - Event Store (Resumability)', () => { + let transport: WebStandardStreamableHTTPServerTransport; + let mcpServer: McpServer; + let eventStore: EventStore; + let storedEvents: Map; + let sessionId: string; - // Send request with different but supported protocol version - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2024-11-05' // Different but supported version - }, - body: JSON.stringify(TEST_MESSAGES.toolsList) - }); + beforeEach(async () => { + storedEvents = new Map(); - // Request should still succeed - expect(response.status).toBe(200); - }); + eventStore = { + async storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise { + const eventId = `${streamId}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + storedEvents.set(eventId, { streamId, message }); + return eventId; + }, + async getStreamIdForEventId(eventId: EventId): Promise { + const event = storedEvents.get(eventId); + return event?.streamId; + }, + async replayEventsAfter( + lastEventId: EventId, + { send }: { send: (eventId: EventId, message: JSONRPCMessage) => Promise } + ): Promise { + const lastEvent = storedEvents.get(lastEventId); + if (!lastEvent) { + throw new Error('Event not found'); + } - it('should reject unsupported protocol version on GET requests', async () => { - sessionId = await initializeServer(); + // Replay events after lastEventId for the same stream + const streamId = lastEvent.streamId; + const entries = Array.from(storedEvents.entries()); + let foundLast = false; - // GET request with unsupported protocol version - const response = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '1999-01-01' // Unsupported version + for (const [eventId, event] of entries) { + if (eventId === lastEventId) { + foundLast = true; + continue; + } + if (foundLast && event.streamId === streamId) { + await send(eventId, event.message); + } } - }); - - expect(response.status).toBe(400); - const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); - }); - it('should reject unsupported protocol version on DELETE requests', async () => { - sessionId = await initializeServer(); + return streamId; + } + }; - // DELETE request with unsupported protocol version - const response = await fetch(baseUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '1999-01-01' // Unsupported version - } - }); + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); - expect(response.status).toBe(400); - const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + mcpServer.tool('greet', 'Greeting tool', { name: z.string() }, async ({ name }): Promise => { + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; }); - }); - }); - describe('NodeStreamableHTTPServerTransport with AuthInfo', () => { - let server: Server; - let transport: NodeStreamableHTTPServerTransport; - let baseUrl: URL; - let sessionId: string; + transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + eventStore + }); - beforeEach(async () => { - const result = await createTestAuthServer(); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; + await mcpServer.connect(transport); }); afterEach(async () => { - await stopTestServer({ server, transport }); + await transport.close(); }); async function initializeServer(): Promise { - const response = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); expect(response.status).toBe(200); const newSessionId = response.headers.get('mcp-session-id'); @@ -1007,1981 +673,85 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { return newSessionId as string; } - it('should call a tool with authInfo', async () => { + it('should store events when event store is configured', async () => { sessionId = await initializeServer(); - const toolCallMessage: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'profile', - arguments: { active: true } - }, - id: 'call-1' - }; - - const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId, { authorization: 'Bearer test-token' }); - expect(response.status).toBe(200); - - const text = await readSSEEvent(response); - const eventLines = text.split('\n'); - const dataLine = eventLines.find(line => line.startsWith('data:')); - expect(dataLine).toBeDefined(); + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId }); + await transport.handleRequest(request); - const eventData = JSON.parse(dataLine!.substring(5)); - expect(eventData).toMatchObject({ - jsonrpc: '2.0', - result: { - content: [ - { - type: 'text', - text: 'Active profile from token: test-token!' - } - ] - }, - id: 'call-1' - }); + // Events should have been stored (priming event + response) + expect(storedEvents.size).toBeGreaterThan(0); }); - it('should calls tool without authInfo when it is optional', async () => { + it('should include event ID in SSE events', async () => { sessionId = await initializeServer(); - const toolCallMessage: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'profile', - arguments: { active: false } - }, - id: 'call-1' - }; - - const response = await sendPostRequest(baseUrl, toolCallMessage, sessionId); - expect(response.status).toBe(200); + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId }); + const response = await transport.handleRequest(request); const text = await readSSEEvent(response); - const eventLines = text.split('\n'); - const dataLine = eventLines.find(line => line.startsWith('data:')); - expect(dataLine).toBeDefined(); - const eventData = JSON.parse(dataLine!.substring(5)); - expect(eventData).toMatchObject({ - jsonrpc: '2.0', - result: { - content: [ - { - type: 'text', - text: 'Inactive profile from token: undefined!' - } - ] - }, - id: 'call-1' - }); + // Should have id: field in the SSE event + expect(text).toContain('id:'); }); }); - // Test JSON Response Mode - describe('NodeStreamableHTTPServerTransport with JSON Response Mode', () => { - let server: Server; - let transport: NodeStreamableHTTPServerTransport; - let baseUrl: URL; + describe('HTTPServerTransport - Protocol Version Validation', () => { + let transport: WebStandardStreamableHTTPServerTransport; + let mcpServer: McpServer; let sessionId: string; beforeEach(async () => { - const result = await createTestServer({ sessionIdGenerator: () => randomUUID(), enableJsonResponse: true }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - // Initialize and get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - - sessionId = initResponse.headers.get('mcp-session-id') as string; - }); - - afterEach(async () => { - await stopTestServer({ server, transport }); - }); - - it('should return JSON response for a single request', async () => { - const toolsListMessage: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 'json-req-1' - }; + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: {} }); - const response = await sendPostRequest(baseUrl, toolsListMessage, sessionId); - - expect(response.status).toBe(200); - expect(response.headers.get('content-type')).toBe('application/json'); - - const result = await response.json(); - expect(result).toMatchObject({ - jsonrpc: '2.0', - result: expect.objectContaining({ - tools: expect.arrayContaining([expect.objectContaining({ name: 'greet' })]) - }), - id: 'json-req-1' - }); - }); - - it('should return JSON response for batch requests', async () => { - const batchMessages: JSONRPCMessage[] = [ - { jsonrpc: '2.0', method: 'tools/list', params: {}, id: 'batch-1' }, - { jsonrpc: '2.0', method: 'tools/call', params: { name: 'greet', arguments: { name: 'JSON' } }, id: 'batch-2' } - ]; - - const response = await sendPostRequest(baseUrl, batchMessages, sessionId); - - expect(response.status).toBe(200); - expect(response.headers.get('content-type')).toBe('application/json'); - - const results = (await response.json()) as JSONRPCResultResponse[]; - expect(Array.isArray(results)).toBe(true); - expect(results).toHaveLength(2); - - // Batch responses can come in any order - const listResponse = results.find((r: { id?: RequestId }) => r.id === 'batch-1'); - const callResponse = results.find((r: { id?: RequestId }) => r.id === 'batch-2'); - - expect(listResponse).toEqual( - expect.objectContaining({ - jsonrpc: '2.0', - id: 'batch-1', - result: expect.objectContaining({ - tools: expect.arrayContaining([expect.objectContaining({ name: 'greet' })]) - }) - }) - ); - - expect(callResponse).toEqual( - expect.objectContaining({ - jsonrpc: '2.0', - id: 'batch-2', - result: expect.objectContaining({ - content: expect.arrayContaining([expect.objectContaining({ type: 'text', text: 'Hello, JSON!' })]) - }) - }) - ); - }); - }); - - // Test pre-parsed body handling - describe('NodeStreamableHTTPServerTransport with pre-parsed body', () => { - let server: Server; - let transport: NodeStreamableHTTPServerTransport; - let baseUrl: URL; - let sessionId: string; - let parsedBody: unknown = null; - - beforeEach(async () => { - const result = await createTestServer({ - customRequestHandler: async (req, res) => { - try { - if (parsedBody !== null) { - await transport.handleRequest(req, res, parsedBody); - parsedBody = null; // Reset after use - } else { - await transport.handleRequest(req, res); - } - } catch (error) { - console.error('Error handling request:', error); - if (!res.headersSent) res.writeHead(500).end(); - } - }, + transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - // Initialize and get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; + await mcpServer.connect(transport); }); afterEach(async () => { - await stopTestServer({ server, transport }); + await transport.close(); }); - it('should accept pre-parsed request body', async () => { - // Set up the pre-parsed body - parsedBody = { - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 'preparsed-1' - }; + async function initializeServer(): Promise { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); + return response.headers.get('mcp-session-id') as string; + } + + it('should reject unsupported protocol version in header', async () => { + sessionId = await initializeServer(); - // Send an empty body since we'll use pre-parsed body - const response = await fetch(baseUrl, { + const request = new Request('http://localhost/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId + 'mcp-session-id': sessionId, + 'mcp-protocol-version': 'unsupported-version' }, - // Empty body - we're testing pre-parsed body - body: '' + body: JSON.stringify(TEST_MESSAGES.toolsList) }); - expect(response.status).toBe(200); - expect(response.headers.get('content-type')).toBe('text/event-stream'); - - const reader = response.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); + const response = await transport.handleRequest(request); - // Verify the response used the pre-parsed body - expect(text).toContain('"id":"preparsed-1"'); - expect(text).toContain('"tools"'); + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Unsupported protocol version/); }); + }); - it('should handle pre-parsed batch messages', async () => { - parsedBody = [ - { jsonrpc: '2.0', method: 'tools/list', params: {}, id: 'batch-1' }, - { jsonrpc: '2.0', method: 'tools/call', params: { name: 'greet', arguments: { name: 'PreParsed' } }, id: 'batch-2' } - ]; - - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId - }, - body: '' // Empty as we're using pre-parsed + describe('HTTPServerTransport - start() method', () => { + it('should throw error when started twice', async () => { + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID() }); - expect(response.status).toBe(200); - - const reader = response.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); + await transport.start(); - expect(text).toContain('"id":"batch-1"'); - expect(text).toContain('"tools"'); - }); - - it('should prefer pre-parsed body over request body', async () => { - // Set pre-parsed to tools/list - parsedBody = { - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 'preparsed-wins' - }; - - // Send actual body with tools/call - should be ignored - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': sessionId - }, - body: JSON.stringify({ - jsonrpc: '2.0', - method: 'tools/call', - params: { name: 'greet', arguments: { name: 'Ignored' } }, - id: 'ignored-id' - }) - }); - - expect(response.status).toBe(200); - - const reader = response.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); - - // Should have processed the pre-parsed body - expect(text).toContain('"id":"preparsed-wins"'); - expect(text).toContain('"tools"'); - expect(text).not.toContain('"ignored-id"'); - }); - }); - - // Test resumability support - describe('NodeStreamableHTTPServerTransport with resumability', () => { - let server: Server; - let transport: NodeStreamableHTTPServerTransport; - let baseUrl: URL; - let sessionId: string; - let mcpServer: McpServer; - const storedEvents: Map = new Map(); - - // Simple implementation of EventStore - const eventStore: EventStore = { - async storeEvent(streamId: string, message: JSONRPCMessage): Promise { - const eventId = `${streamId}_${randomUUID()}`; - storedEvents.set(eventId, { eventId, message }); - return eventId; - }, - - async replayEventsAfter( - lastEventId: EventId, - { - send - }: { - send: (eventId: EventId, message: JSONRPCMessage) => Promise; - } - ): Promise { - const streamId = lastEventId.split('_')[0]!; - // Extract stream ID from the event ID - // For test simplicity, just return all events with matching streamId that aren't the lastEventId - for (const [eventId, { message }] of storedEvents.entries()) { - if (eventId.startsWith(streamId) && eventId !== lastEventId) { - await send(eventId, message); - } - } - return streamId; - } - }; - - beforeEach(async () => { - storedEvents.clear(); - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore - }); - - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Initialize the server - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - }); - - afterEach(async () => { - await stopTestServer({ server, transport }); - storedEvents.clear(); - }); - - it('should store and include event IDs in server SSE messages', async () => { - // Open a standalone SSE stream - const sseResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } - }); - - expect(sseResponse.status).toBe(200); - expect(sseResponse.headers.get('content-type')).toBe('text/event-stream'); - - // Send a notification that should be stored with an event ID - const notification: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'notifications/message', - params: { level: 'info', data: 'Test notification with event ID' } - }; - - // Send the notification via transport - await transport.send(notification); - - // Read from the stream and verify we got the notification with an event ID - const reader = sseResponse.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); - - // The response should contain an event ID - expect(text).toContain('id: '); - expect(text).toContain('"method":"notifications/message"'); - - // Extract the event ID - const idMatch = text.match(/id: ([^\n]+)/); - expect(idMatch).toBeTruthy(); - - // Verify the event was stored - const eventId = idMatch![1]!; - expect(storedEvents.has(eventId)).toBe(true); - const storedEvent = storedEvents.get(eventId); - expect(eventId.startsWith('_GET_stream')).toBe(true); - expect(storedEvent?.message).toMatchObject(notification); - }); - - it('should store and replay MCP server tool notifications', async () => { - // Establish a standalone SSE stream - const sseResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } - }); - expect(sseResponse.status).toBe(200); - - // Send a server notification through the MCP server - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'First notification from MCP server' }); - - // Read the notification from the SSE stream - const reader = sseResponse.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); - - // Verify the notification was sent with an event ID - expect(text).toContain('id: '); - expect(text).toContain('First notification from MCP server'); - - // Extract the event ID - const idMatch = text.match(/id: ([^\n]+)/); - expect(idMatch).toBeTruthy(); - const firstEventId = idMatch![1]!; - - // Send a second notification - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Second notification from MCP server' }); - - // Close the first SSE stream to simulate a disconnect - await reader!.cancel(); - - // Reconnect with the Last-Event-ID to get missed messages - const reconnectResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25', - 'last-event-id': firstEventId - } - }); - - expect(reconnectResponse.status).toBe(200); - - // Read the replayed notification - const reconnectReader = reconnectResponse.body?.getReader(); - const reconnectData = await reconnectReader!.read(); - const reconnectText = new TextDecoder().decode(reconnectData.value); - - // Verify we received the second notification that was sent after our stored eventId - expect(reconnectText).toContain('Second notification from MCP server'); - expect(reconnectText).toContain('id: '); - }); - - it('should store and replay multiple notifications sent while client is disconnected', async () => { - // Establish a standalone SSE stream - const sseResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } - }); - expect(sseResponse.status).toBe(200); - - const reader = sseResponse.body?.getReader(); - - // Send a notification to get an event ID - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Initial notification' }); - - // Read the notification from the SSE stream - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); - - // Extract the event ID - const idMatch = text.match(/id: ([^\n]+)/); - expect(idMatch).toBeTruthy(); - const lastEventId = idMatch![1]!; - - // Close the SSE stream to simulate a disconnect - await reader!.cancel(); - - // Send MULTIPLE notifications while the client is disconnected - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Missed notification 1' }); - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Missed notification 2' }); - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Missed notification 3' }); - - // Reconnect with the Last-Event-ID to get all missed messages - const reconnectResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25', - 'last-event-id': lastEventId - } - }); - - expect(reconnectResponse.status).toBe(200); - - // Read replayed notifications with a timeout - const reconnectReader = reconnectResponse.body?.getReader(); - let allText = ''; - - // Read chunks until we have all 3 notifications or timeout - const readWithTimeout = async () => { - const timeout = setTimeout(() => reconnectReader!.cancel(), 2000); - try { - while (!allText.includes('Missed notification 3')) { - const { value, done } = await reconnectReader!.read(); - if (done) break; - allText += new TextDecoder().decode(value); - } - } finally { - clearTimeout(timeout); - } - }; - await readWithTimeout(); - - // Verify we received ALL notifications that were sent while disconnected - expect(allText).toContain('Missed notification 1'); - expect(allText).toContain('Missed notification 2'); - expect(allText).toContain('Missed notification 3'); - }); - }); - - // Test stateless mode - describe('NodeStreamableHTTPServerTransport in stateless mode', () => { - let server: Server; - let transport: NodeStreamableHTTPServerTransport; - let baseUrl: URL; - - beforeEach(async () => { - const result = await createTestServer({ sessionIdGenerator: undefined }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - }); - - afterEach(async () => { - await stopTestServer({ server, transport }); - }); - - it('should operate without session ID validation', async () => { - // Initialize the server first - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - - expect(initResponse.status).toBe(200); - // Should NOT have session ID header in stateless mode - expect(initResponse.headers.get('mcp-session-id')).toBeNull(); - - // Try request without session ID - should work in stateless mode - const toolsResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList); - - expect(toolsResponse.status).toBe(200); - }); - - it('should handle POST requests with various session IDs in stateless mode', async () => { - await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - - // Try with a random session ID - should be accepted - const response1 = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': 'random-id-1' - }, - body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', params: {}, id: 't1' }) - }); - expect(response1.status).toBe(200); - - // Try with another random session ID - should also be accepted - const response2 = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'mcp-session-id': 'different-id-2' - }, - body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', params: {}, id: 't2' }) - }); - expect(response2.status).toBe(200); - }); - - it('should reject second SSE stream even in stateless mode', async () => { - // Despite no session ID requirement, the transport still only allows - // one standalone SSE stream at a time - - // Initialize the server first - await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - - // Open first SSE stream - const stream1 = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-protocol-version': '2025-11-25' - } - }); - expect(stream1.status).toBe(200); - - // Open second SSE stream - should still be rejected, stateless mode still only allows one - const stream2 = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-protocol-version': '2025-11-25' - } - }); - expect(stream2.status).toBe(409); // Conflict - only one stream allowed - }); - }); - - // Test SSE priming events for POST streams - describe('NodeStreamableHTTPServerTransport POST SSE priming events', () => { - let server: Server; - let transport: NodeStreamableHTTPServerTransport; - let baseUrl: URL; - let sessionId: string; - let mcpServer: McpServer; - - // Simple eventStore for priming event tests - const createEventStore = (): EventStore => { - const storedEvents = new Map(); - return { - async storeEvent(streamId: string, message: JSONRPCMessage): Promise { - const eventId = `${streamId}::${Date.now()}_${randomUUID()}`; - storedEvents.set(eventId, { eventId, message, streamId }); - return eventId; - }, - async getStreamIdForEventId(eventId: string): Promise { - const event = storedEvents.get(eventId); - return event?.streamId; - }, - async replayEventsAfter( - lastEventId: EventId, - { send }: { send: (eventId: EventId, message: JSONRPCMessage) => Promise } - ): Promise { - const event = storedEvents.get(lastEventId); - const streamId = event?.streamId || lastEventId.split('::')[0]!; - const eventsToReplay: Array<[string, { message: JSONRPCMessage }]> = []; - for (const [eventId, data] of storedEvents.entries()) { - if (data.streamId === streamId && eventId > lastEventId) { - eventsToReplay.push([eventId, data]); - } - } - eventsToReplay.sort(([a], [b]) => a.localeCompare(b)); - for (const [eventId, { message }] of eventsToReplay) { - if (Object.keys(message).length > 0) { - await send(eventId, message); - } - } - return streamId; - } - }; - }; - - afterEach(async () => { - if (server && transport) { - await stopTestServer({ server, transport }); - } - }); - - it('should send priming event with retry field on POST SSE stream', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore(), - retryInterval: 5000 - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Send a tool call request - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 100, - method: 'tools/call', - params: { name: 'greet', arguments: { name: 'Test' } } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - }, - body: JSON.stringify(toolCallRequest) - }); - - expect(postResponse.status).toBe(200); - expect(postResponse.headers.get('content-type')).toBe('text/event-stream'); - - // Read the priming event - const reader = postResponse.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); - - // Verify priming event has id and retry field - expect(text).toContain('id: '); - expect(text).toContain('retry: 5000'); - expect(text).toContain('data: '); - }); - - it('should NOT send priming event for old protocol versions (backwards compatibility)', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore(), - retryInterval: 5000 - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Initialize with OLD protocol version to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initializeOldVersion); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Send a tool call request with the same OLD protocol version - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 100, - method: 'tools/call', - params: { name: 'greet', arguments: { name: 'Test' } } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-06-18' - }, - body: JSON.stringify(toolCallRequest) - }); - - expect(postResponse.status).toBe(200); - expect(postResponse.headers.get('content-type')).toBe('text/event-stream'); - - // Read the first chunk - should be the actual response, not a priming event - const reader = postResponse.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); - - // Should NOT contain a priming event (empty data line before the response) - // The first message should be the actual tool result - expect(text).toContain('event: message'); - expect(text).toContain('"result"'); - // Should NOT have a separate priming event line with empty data - expect(text).not.toMatch(/^id:.*\ndata:\s*\n\n/); - }); - - it('should send priming event without retry field when retryInterval is not configured', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore() - // No retryInterval - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Send a tool call request - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 100, - method: 'tools/call', - params: { name: 'greet', arguments: { name: 'Test' } } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - }, - body: JSON.stringify(toolCallRequest) - }); - - expect(postResponse.status).toBe(200); - - // Read the priming event - const reader = postResponse.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); - - // Priming event should have id field but NOT retry field - expect(text).toContain('id: '); - expect(text).toContain('data: '); - expect(text).not.toContain('retry:'); - }); - - it('should close POST SSE stream when extra.closeSSEStream is called', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore(), - retryInterval: 1000 - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Track when stream close is called and tool completes - let streamCloseCalled = false; - let toolResolve: () => void; - const toolCompletePromise = new Promise(resolve => { - toolResolve = resolve; - }); - - // Register a tool that closes its own SSE stream via extra callback - mcpServer.tool('close-stream-tool', 'Closes its own stream', {}, async (_args, extra) => { - // Close the SSE stream for this request - extra.closeSSEStream?.(); - streamCloseCalled = true; - - // Wait before returning so we can observe the stream closure - await toolCompletePromise; - return { content: [{ type: 'text', text: 'Done' }] }; - }); - - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Send a tool call request - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 100, - method: 'tools/call', - params: { name: 'close-stream-tool', arguments: {} } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - }, - body: JSON.stringify(toolCallRequest) - }); - - expect(postResponse.status).toBe(200); - - const reader = postResponse.body?.getReader(); - - // Read the priming event - await reader!.read(); - - // Wait a moment for the tool to call closeSSEStream - await new Promise(resolve => setTimeout(resolve, 100)); - expect(streamCloseCalled).toBe(true); - - // Stream should now be closed - const { done } = await reader!.read(); - expect(done).toBe(true); - - // Clean up - resolve the tool promise - toolResolve!(); - }); - - it('should provide closeSSEStream callback in extra when eventStore is configured', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore(), - retryInterval: 1000 - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Track whether closeSSEStream callback was provided - let receivedCloseSSEStream: (() => void) | undefined; - - // Register a tool that captures the extra.closeSSEStream callback - mcpServer.tool('test-callback-tool', 'Test tool', {}, async (_args, extra) => { - receivedCloseSSEStream = extra.closeSSEStream; - return { content: [{ type: 'text', text: 'Done' }] }; - }); - - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Call the tool - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 200, - method: 'tools/call', - params: { name: 'test-callback-tool', arguments: {} } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - }, - body: JSON.stringify(toolCallRequest) - }); - - expect(postResponse.status).toBe(200); - - // Read all events to completion - const reader = postResponse.body?.getReader(); - while (true) { - const { done } = await reader!.read(); - if (done) break; - } - - // Verify closeSSEStream callback was provided - expect(receivedCloseSSEStream).toBeDefined(); - expect(typeof receivedCloseSSEStream).toBe('function'); - }); - - it('should NOT provide closeSSEStream callback for old protocol versions (backwards compatibility)', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore(), - retryInterval: 1000 - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Track whether closeSSEStream callback was provided - let receivedCloseSSEStream: (() => void) | undefined; - let receivedCloseStandaloneSSEStream: (() => void) | undefined; - - // Register a tool that captures the extra.closeSSEStream callback - mcpServer.tool('test-old-version-tool', 'Test tool', {}, async (_args, extra) => { - receivedCloseSSEStream = extra.closeSSEStream; - receivedCloseStandaloneSSEStream = extra.closeStandaloneSSEStream; - return { content: [{ type: 'text', text: 'Done' }] }; - }); - - // Initialize with OLD protocol version to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initializeOldVersion); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Call the tool with the same OLD protocol version - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 200, - method: 'tools/call', - params: { name: 'test-old-version-tool', arguments: {} } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-06-18' - }, - body: JSON.stringify(toolCallRequest) - }); - - expect(postResponse.status).toBe(200); - - // Read all events to completion - const reader = postResponse.body?.getReader(); - while (true) { - const { done } = await reader!.read(); - if (done) break; - } - - // Verify closeSSEStream callbacks were NOT provided for old protocol version - // even though eventStore is configured - expect(receivedCloseSSEStream).toBeUndefined(); - expect(receivedCloseStandaloneSSEStream).toBeUndefined(); - }); - - it('should NOT provide closeSSEStream callback when eventStore is NOT configured', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID() - // No eventStore - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Track whether closeSSEStream callback was provided - let receivedCloseSSEStream: (() => void) | undefined; - - // Register a tool that captures the extra.closeSSEStream callback - mcpServer.tool('test-no-callback-tool', 'Test tool', {}, async (_args, extra) => { - receivedCloseSSEStream = extra.closeSSEStream; - return { content: [{ type: 'text', text: 'Done' }] }; - }); - - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Call the tool - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 201, - method: 'tools/call', - params: { name: 'test-no-callback-tool', arguments: {} } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - }, - body: JSON.stringify(toolCallRequest) - }); - - expect(postResponse.status).toBe(200); - - // Read all events to completion - const reader = postResponse.body?.getReader(); - while (true) { - const { done } = await reader!.read(); - if (done) break; - } - - // Verify closeSSEStream callback was NOT provided - expect(receivedCloseSSEStream).toBeUndefined(); - }); - - it('should provide closeStandaloneSSEStream callback in extra when eventStore is configured', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore(), - retryInterval: 1000 - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Track whether closeStandaloneSSEStream callback was provided - let receivedCloseStandaloneSSEStream: (() => void) | undefined; - - // Register a tool that captures the extra.closeStandaloneSSEStream callback - mcpServer.tool('test-standalone-callback-tool', 'Test tool', {}, async (_args, extra) => { - receivedCloseStandaloneSSEStream = extra.closeStandaloneSSEStream; - return { content: [{ type: 'text', text: 'Done' }] }; - }); - - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Call the tool - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 203, - method: 'tools/call', - params: { name: 'test-standalone-callback-tool', arguments: {} } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - }, - body: JSON.stringify(toolCallRequest) - }); - - expect(postResponse.status).toBe(200); - - // Read all events to completion - const reader = postResponse.body?.getReader(); - while (true) { - const { done } = await reader!.read(); - if (done) break; - } - - // Verify closeStandaloneSSEStream callback was provided - expect(receivedCloseStandaloneSSEStream).toBeDefined(); - expect(typeof receivedCloseStandaloneSSEStream).toBe('function'); - }); - - it('should close standalone GET SSE stream when extra.closeStandaloneSSEStream is called', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore(), - retryInterval: 1000 - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Register a tool that closes the standalone SSE stream via extra callback - mcpServer.tool('close-standalone-stream-tool', 'Closes standalone stream', {}, async (_args, extra) => { - extra.closeStandaloneSSEStream?.(); - return { content: [{ type: 'text', text: 'Stream closed' }] }; - }); - - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Open a standalone GET SSE stream - const sseResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } - }); - expect(sseResponse.status).toBe(200); - - const getReader = sseResponse.body?.getReader(); - - // Send a notification to confirm GET stream is established - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Stream established' }); - - // Read the notification to confirm stream is working - const { value } = await getReader!.read(); - const text = new TextDecoder().decode(value); - expect(text).toContain('id: '); - expect(text).toContain('Stream established'); - - // Call the tool that closes the standalone SSE stream - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 300, - method: 'tools/call', - params: { name: 'close-standalone-stream-tool', arguments: {} } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - }, - body: JSON.stringify(toolCallRequest) - }); - expect(postResponse.status).toBe(200); - - // Read the POST response to completion - const postReader = postResponse.body?.getReader(); - while (true) { - const { done } = await postReader!.read(); - if (done) break; - } - - // GET stream should now be closed - use a race with timeout to avoid hanging - const readPromise = getReader!.read(); - const timeoutPromise = new Promise<{ done: boolean; value: undefined }>((_, reject) => - setTimeout(() => reject(new Error('Stream did not close in time')), 1000) - ); - - const { done } = await Promise.race([readPromise, timeoutPromise]); - expect(done).toBe(true); - }); - - it('should allow client to reconnect after standalone SSE stream is closed via extra.closeStandaloneSSEStream', async () => { - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - eventStore: createEventStore(), - retryInterval: 1000 - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - mcpServer = result.mcpServer; - - // Register a tool that closes the standalone SSE stream - mcpServer.tool('close-standalone-for-reconnect', 'Closes standalone stream', {}, async (_args, extra) => { - extra.closeStandaloneSSEStream?.(); - return { content: [{ type: 'text', text: 'Stream closed' }] }; - }); - - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - sessionId = initResponse.headers.get('mcp-session-id') as string; - expect(sessionId).toBeDefined(); - - // Open a standalone GET SSE stream - const sseResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - } - }); - expect(sseResponse.status).toBe(200); - - const getReader = sseResponse.body?.getReader(); - - // Send a notification to get an event ID - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Initial message' }); - - // Read the notification to get the event ID - const { value } = await getReader!.read(); - const text = new TextDecoder().decode(value); - const idMatch = text.match(/id: ([^\n]+)/); - expect(idMatch).toBeTruthy(); - const lastEventId = idMatch![1]!; - - // Call the tool to close the standalone SSE stream - const toolCallRequest: JSONRPCMessage = { - jsonrpc: '2.0', - id: 301, - method: 'tools/call', - params: { name: 'close-standalone-for-reconnect', arguments: {} } - }; - - const postResponse = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream, application/json', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25' - }, - body: JSON.stringify(toolCallRequest) - }); - expect(postResponse.status).toBe(200); - - // Read the POST response to completion - const postReader = postResponse.body?.getReader(); - while (true) { - const { done } = await postReader!.read(); - if (done) break; - } - - // Wait for GET stream to close - use a race with timeout - const readPromise = getReader!.read(); - const timeoutPromise = new Promise<{ done: boolean; value: undefined }>((_, reject) => - setTimeout(() => reject(new Error('Stream did not close in time')), 1000) - ); - const { done } = await Promise.race([readPromise, timeoutPromise]); - expect(done).toBe(true); - - // Send a notification while client is disconnected - await mcpServer.server.sendLoggingMessage({ level: 'info', data: 'Missed while disconnected' }); - - // Client reconnects with Last-Event-ID - const reconnectResponse = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream', - 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-11-25', - 'last-event-id': lastEventId - } - }); - expect(reconnectResponse.status).toBe(200); - - // Read the replayed notification - const reconnectReader = reconnectResponse.body?.getReader(); - let allText = ''; - const readWithTimeout = async () => { - const timeout = setTimeout(() => reconnectReader!.cancel(), 5000); - try { - while (!allText.includes('Missed while disconnected')) { - const { value, done } = await reconnectReader!.read(); - if (done) break; - allText += new TextDecoder().decode(value); - } - } finally { - clearTimeout(timeout); - } - }; - await readWithTimeout(); - - // Verify we received the notification that was sent while disconnected - expect(allText).toContain('Missed while disconnected'); - }, 15000); - }); - - // Test onsessionclosed callback - describe('NodeStreamableHTTPServerTransport onsessionclosed callback', () => { - it('should call onsessionclosed callback when session is closed via DELETE', async () => { - const mockCallback = vi.fn(); - - // Create server with onsessionclosed callback - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessionclosed: mockCallback - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize to get a session ID - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - const tempSessionId = initResponse.headers.get('mcp-session-id'); - expect(tempSessionId).toBeDefined(); - - // DELETE the session - const deleteResponse = await fetch(tempUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-11-25' - } - }); - - expect(deleteResponse.status).toBe(200); - expect(mockCallback).toHaveBeenCalledWith(tempSessionId); - expect(mockCallback).toHaveBeenCalledTimes(1); - - // Clean up - tempServer.close(); - }); - - it('should not call onsessionclosed callback when not provided', async () => { - // Create server without onsessionclosed callback - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID() - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize to get a session ID - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - const tempSessionId = initResponse.headers.get('mcp-session-id'); - - // DELETE the session - should not throw error - const deleteResponse = await fetch(tempUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-03-26' - } - }); - - expect(deleteResponse.status).toBe(200); - - // Clean up - tempServer.close(); - }); - - it('should not call onsessionclosed callback for invalid session DELETE', async () => { - const mockCallback = vi.fn(); - - // Create server with onsessionclosed callback - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessionclosed: mockCallback - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize to get a valid session - await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - - // Try to DELETE with invalid session ID - const deleteResponse = await fetch(tempUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': 'invalid-session-id', - 'mcp-protocol-version': '2025-11-25' - } - }); - - expect(deleteResponse.status).toBe(404); - expect(mockCallback).not.toHaveBeenCalled(); - - // Clean up - tempServer.close(); - }); - - it('should call onsessionclosed callback with correct session ID when multiple sessions exist', async () => { - const mockCallback = vi.fn(); - - // Create first server - const result1 = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessionclosed: mockCallback - }); - - const server1 = result1.server; - const url1 = result1.baseUrl; - - // Create second server - const result2 = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessionclosed: mockCallback - }); - - const server2 = result2.server; - const url2 = result2.baseUrl; - - // Initialize both servers - const initResponse1 = await sendPostRequest(url1, TEST_MESSAGES.initialize); - const sessionId1 = initResponse1.headers.get('mcp-session-id'); - - const initResponse2 = await sendPostRequest(url2, TEST_MESSAGES.initialize); - const sessionId2 = initResponse2.headers.get('mcp-session-id'); - - expect(sessionId1).toBeDefined(); - expect(sessionId2).toBeDefined(); - expect(sessionId1).not.toBe(sessionId2); - - // DELETE first session - const deleteResponse1 = await fetch(url1, { - method: 'DELETE', - headers: { - 'mcp-session-id': sessionId1 || '', - 'mcp-protocol-version': '2025-11-25' - } - }); - - expect(deleteResponse1.status).toBe(200); - expect(mockCallback).toHaveBeenCalledWith(sessionId1); - expect(mockCallback).toHaveBeenCalledTimes(1); - - // DELETE second session - const deleteResponse2 = await fetch(url2, { - method: 'DELETE', - headers: { - 'mcp-session-id': sessionId2 || '', - 'mcp-protocol-version': '2025-11-25' - } - }); - - expect(deleteResponse2.status).toBe(200); - expect(mockCallback).toHaveBeenCalledWith(sessionId2); - expect(mockCallback).toHaveBeenCalledTimes(2); - - // Clean up - server1.close(); - server2.close(); - }); - }); - - // Test async callbacks for onsessioninitialized and onsessionclosed - describe('NodeStreamableHTTPServerTransport async callbacks', () => { - it('should support async onsessioninitialized callback', async () => { - const initializationOrder: string[] = []; - - // Create server with async onsessioninitialized callback - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: async (sessionId: string) => { - initializationOrder.push('async-start'); - // Simulate async operation - await new Promise(resolve => setTimeout(resolve, 10)); - initializationOrder.push('async-end'); - initializationOrder.push(sessionId); - } - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize to trigger the callback - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - const tempSessionId = initResponse.headers.get('mcp-session-id'); - - // Give time for async callback to complete - await new Promise(resolve => setTimeout(resolve, 50)); - - expect(initializationOrder).toEqual(['async-start', 'async-end', tempSessionId]); - - // Clean up - tempServer.close(); - }); - - it('should support sync onsessioninitialized callback (backwards compatibility)', async () => { - const capturedSessionId: string[] = []; - - // Create server with sync onsessioninitialized callback - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (sessionId: string) => { - capturedSessionId.push(sessionId); - } - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize to trigger the callback - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - const tempSessionId = initResponse.headers.get('mcp-session-id'); - - expect(capturedSessionId).toEqual([tempSessionId]); - - // Clean up - tempServer.close(); - }); - - it('should support async onsessionclosed callback', async () => { - const closureOrder: string[] = []; - - // Create server with async onsessionclosed callback - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessionclosed: async (sessionId: string) => { - closureOrder.push('async-close-start'); - // Simulate async operation - await new Promise(resolve => setTimeout(resolve, 10)); - closureOrder.push('async-close-end'); - closureOrder.push(sessionId); - } - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize to get a session ID - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - const tempSessionId = initResponse.headers.get('mcp-session-id'); - expect(tempSessionId).toBeDefined(); - - // DELETE the session - const deleteResponse = await fetch(tempUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-11-25' - } - }); - - expect(deleteResponse.status).toBe(200); - - // Give time for async callback to complete - await new Promise(resolve => setTimeout(resolve, 50)); - - expect(closureOrder).toEqual(['async-close-start', 'async-close-end', tempSessionId]); - - // Clean up - tempServer.close(); - }); - - it('should propagate errors from async onsessioninitialized callback', async () => { - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - // Create server with async onsessioninitialized callback that throws - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: async (_sessionId: string) => { - throw new Error('Async initialization error'); - } - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize should fail when callback throws - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - expect(initResponse.status).toBe(400); - - // Clean up - consoleErrorSpy.mockRestore(); - tempServer.close(); - }); - - it('should propagate errors from async onsessionclosed callback', async () => { - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - // Create server with async onsessionclosed callback that throws - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessionclosed: async (_sessionId: string) => { - throw new Error('Async closure error'); - } - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize to get a session ID - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - const tempSessionId = initResponse.headers.get('mcp-session-id'); - - // DELETE should fail when callback throws - const deleteResponse = await fetch(tempUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-11-25' - } - }); - - expect(deleteResponse.status).toBe(500); - - // Clean up - consoleErrorSpy.mockRestore(); - tempServer.close(); - }); - - it('should handle both async callbacks together', async () => { - const events: string[] = []; - - // Create server with both async callbacks - const result = await createTestServer({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: async (sessionId: string) => { - await new Promise(resolve => setTimeout(resolve, 5)); - events.push(`initialized:${sessionId}`); - }, - onsessionclosed: async (sessionId: string) => { - await new Promise(resolve => setTimeout(resolve, 5)); - events.push(`closed:${sessionId}`); - } - }); - - const tempServer = result.server; - const tempUrl = result.baseUrl; - - // Initialize to trigger first callback - const initResponse = await sendPostRequest(tempUrl, TEST_MESSAGES.initialize); - const tempSessionId = initResponse.headers.get('mcp-session-id'); - - // Wait for async callback - await new Promise(resolve => setTimeout(resolve, 20)); - - expect(events).toContain(`initialized:${tempSessionId}`); - - // DELETE to trigger second callback - const deleteResponse = await fetch(tempUrl, { - method: 'DELETE', - headers: { - 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-11-25' - } - }); - - expect(deleteResponse.status).toBe(200); - - // Wait for async callback - await new Promise(resolve => setTimeout(resolve, 20)); - - expect(events).toContain(`closed:${tempSessionId}`); - expect(events).toHaveLength(2); - - // Clean up - tempServer.close(); - }); - }); - - // Test DNS rebinding protection - describe('NodeStreamableHTTPServerTransport DNS rebinding protection', () => { - let server: Server; - let transport: NodeStreamableHTTPServerTransport; - let baseUrl: URL; - - afterEach(async () => { - if (server && transport) { - await stopTestServer({ server, transport }); - } - }); - - describe('Host header validation', () => { - it('should accept requests with allowed host headers', async () => { - const result = await createTestServerWithDnsProtection({ - sessionIdGenerator: undefined, - allowedHosts: ['localhost'], - enableDnsRebindingProtection: true - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - // Note: fetch() automatically sets Host header to match the URL - // Since we're connecting to localhost:3001 and that's in allowedHosts, this should work - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream' - }, - body: JSON.stringify(TEST_MESSAGES.initialize) - }); - - expect(response.status).toBe(200); - }); - - it('should reject requests with disallowed host headers', async () => { - // Test DNS rebinding protection by creating a server that only allows example.com - // but we're connecting via localhost, so it should be rejected - const result = await createTestServerWithDnsProtection({ - sessionIdGenerator: undefined, - allowedHosts: ['example.com:3001'], - enableDnsRebindingProtection: true - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream' - }, - body: JSON.stringify(TEST_MESSAGES.initialize) - }); - - expect(response.status).toBe(403); - const body = (await response.json()) as JSONRPCErrorResponse; - expect(body.error.message).toContain('Invalid Host header:'); - }); - - it('should reject GET requests with disallowed host headers', async () => { - const result = await createTestServerWithDnsProtection({ - sessionIdGenerator: undefined, - allowedHosts: ['example.com:3001'], - enableDnsRebindingProtection: true - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - const response = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream' - } - }); - - expect(response.status).toBe(403); - }); - }); - - describe('Origin header validation', () => { - it('should accept requests with allowed origin headers', async () => { - const result = await createTestServerWithDnsProtection({ - sessionIdGenerator: undefined, - allowedOrigins: ['http://localhost:3000', 'https://example.com'], - enableDnsRebindingProtection: true - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - Origin: 'http://localhost:3000' - }, - body: JSON.stringify(TEST_MESSAGES.initialize) - }); - - expect(response.status).toBe(200); - }); - - it('should reject requests with disallowed origin headers', async () => { - const result = await createTestServerWithDnsProtection({ - sessionIdGenerator: undefined, - allowedOrigins: ['http://localhost:3000'], - enableDnsRebindingProtection: true - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - Origin: 'http://evil.com' - }, - body: JSON.stringify(TEST_MESSAGES.initialize) - }); - - expect(response.status).toBe(403); - const body = (await response.json()) as JSONRPCErrorResponse; - expect(body.error.message).toBe('Invalid Origin header: http://evil.com'); - }); - - it('should accept requests without origin headers', async () => { - const result = await createTestServerWithDnsProtection({ - sessionIdGenerator: undefined, - allowedOrigins: ['http://localhost:3000', 'https://example.com'], - enableDnsRebindingProtection: true - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream' - }, - body: JSON.stringify(TEST_MESSAGES.initialize) - }); - - // Should pass even with no Origin headers because requests that do not come from browsers may not have Origin and DNS rebinding attacks can only be performed via browsers - expect(response.status).toBe(200); - }); - }); - - describe('enableDnsRebindingProtection option', () => { - it('should skip all validations when enableDnsRebindingProtection is false', async () => { - const result = await createTestServerWithDnsProtection({ - sessionIdGenerator: undefined, - allowedHosts: ['localhost'], - allowedOrigins: ['http://localhost:3000'], - enableDnsRebindingProtection: false - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - const response = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - Host: 'evil.com', - Origin: 'http://evil.com' - }, - body: JSON.stringify(TEST_MESSAGES.initialize) - }); - - // Should pass even with invalid headers because protection is disabled - expect(response.status).toBe(200); - }); - }); - - describe('Combined validations', () => { - it('should validate both host and origin when both are configured', async () => { - const result = await createTestServerWithDnsProtection({ - sessionIdGenerator: undefined, - allowedHosts: ['localhost'], - allowedOrigins: ['http://localhost:3001'], - enableDnsRebindingProtection: true - }); - server = result.server; - transport = result.transport; - baseUrl = result.baseUrl; - - // Test with invalid origin (host will be automatically correct via fetch) - const response1 = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - Origin: 'http://evil.com' - }, - body: JSON.stringify(TEST_MESSAGES.initialize) - }); - - expect(response1.status).toBe(403); - const body1 = (await response1.json()) as JSONRPCErrorResponse; - expect(body1.error.message).toBe('Invalid Origin header: http://evil.com'); - - // Test with valid origin - const response2 = await fetch(baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - Origin: 'http://localhost:3001' - }, - body: JSON.stringify(TEST_MESSAGES.initialize) - }); - - expect(response2.status).toBe(200); - }); + await expect(transport.start()).rejects.toThrow('Transport already started'); }); }); }); - -/** - * Helper to create test server with DNS rebinding protection options - */ -async function createTestServerWithDnsProtection(config: { - sessionIdGenerator: (() => string) | undefined; - allowedHosts?: string[]; - allowedOrigins?: string[]; - enableDnsRebindingProtection?: boolean; -}): Promise<{ - server: Server; - transport: NodeStreamableHTTPServerTransport; - mcpServer: McpServer; - baseUrl: URL; -}> { - const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); - - const port = await getFreePort(); - - if (config.allowedHosts) { - config.allowedHosts = config.allowedHosts.map(host => { - if (host.includes(':')) { - return host; - } - return `localhost:${port}`; - }); - } - - const transport = new NodeStreamableHTTPServerTransport({ - sessionIdGenerator: config.sessionIdGenerator, - allowedHosts: config.allowedHosts, - allowedOrigins: config.allowedOrigins, - enableDnsRebindingProtection: config.enableDnsRebindingProtection - }); - - await mcpServer.connect(transport); - - const httpServer = createServer(async (req, res) => { - if (req.method === 'POST') { - let body = ''; - req.on('data', chunk => (body += chunk)); - req.on('end', async () => { - const parsedBody = JSON.parse(body); - await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res, parsedBody); - }); - } else { - await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res); - } - }); - - await new Promise(resolve => { - httpServer.listen(port, () => resolve()); - }); - - const serverUrl = new URL(`http://localhost:${port}/`); - - return { - server: httpServer, - transport, - mcpServer, - baseUrl: serverUrl - }; -} diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index a16bfd7d9..765ab8f0f 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@modelcontextprotocol/tsconfig", - "include": ["./"], + "include": ["./", "../middleware/node/src/streamableHttp.ts", "../middleware/node/test/streamableHttp.test.ts"], "exclude": ["node_modules", "dist"], "compilerOptions": { "paths": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c37a8e0fc..d1d0d1f44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,7 @@ catalogs: devTools: '@eslint/js': specifier: ^9.39.1 - version: 9.39.1 + version: 9.39.2 '@types/content-type': specifier: ^1.1.8 version: 1.1.9 @@ -23,7 +23,7 @@ catalogs: version: 1.1.15 '@types/express': specifier: ^5.0.0 - version: 5.0.5 + version: 5.0.6 '@types/express-serve-static-core': specifier: ^5.1.0 version: 5.1.0 @@ -35,10 +35,10 @@ catalogs: version: 8.18.1 '@typescript/native-preview': specifier: ^7.0.0-dev.20251217.1 - version: 7.0.0-dev.20251218.3 + version: 7.0.0-dev.20260109.1 eslint: specifier: ^9.8.0 - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8 @@ -50,28 +50,28 @@ catalogs: version: 3.6.2 supertest: specifier: ^7.0.0 - version: 7.1.4 + version: 7.2.2 tsdown: specifier: ^0.18.0 - version: 0.18.0 + version: 0.18.4 tsx: specifier: ^4.16.5 - version: 4.20.6 + version: 4.21.0 typescript: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: specifier: ^8.48.1 - version: 8.49.0 + version: 8.52.0 vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4 vitest: - specifier: ^4.0.8 - version: 4.0.9 + specifier: ^4.0.15 + version: 4.0.16 ws: specifier: ^8.18.0 - version: 8.18.3 + version: 8.19.0 runtimeClientOnly: cross-spawn: specifier: ^7.0.5 @@ -88,7 +88,7 @@ catalogs: runtimeServerOnly: '@hono/node-server': specifier: ^1.19.7 - version: 1.19.7 + version: 1.19.8 '@remix-run/node-fetch-server': specifier: ^0.13.0 version: 0.13.0 @@ -103,10 +103,10 @@ catalogs: version: 5.2.1 hono: specifier: ^4.11.1 - version: 4.11.1 + version: 4.11.3 raw-body: specifier: ^3.0.0 - version: 3.0.1 + version: 3.0.2 runtimeShared: '@cfworker/json-schema': specifier: ^4.1.1 @@ -122,13 +122,13 @@ catalogs: version: 8.0.2 pkce-challenge: specifier: ^5.0.0 - version: 5.0.0 + version: 5.0.1 zod: specifier: ^3.25 || ^4.0 - version: 3.25.76 + version: 4.3.5 zod-to-json-schema: specifier: ^3.25.0 - version: 3.25.0 + version: 3.25.1 overrides: strip-ansi: 6.0.1 @@ -145,16 +145,16 @@ importers: version: 0.5.2 '@changesets/cli': specifier: ^2.29.8 - version: 2.29.8(@types/node@24.10.3) + version: 2.29.8(@types/node@24.10.4) '@eslint/js': specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 '@modelcontextprotocol/client': specifier: workspace:^ version: link:packages/client '@modelcontextprotocol/conformance': specifier: 0.1.9 - version: 0.1.9(@cfworker/json-schema@4.1.1)(hono@4.11.1) + version: 0.1.9(@cfworker/json-schema@4.1.1)(hono@4.11.3) '@types/content-type': specifier: catalog:devTools version: 1.1.9 @@ -169,13 +169,13 @@ importers: version: 1.1.15 '@types/express': specifier: catalog:devTools - version: 5.0.5 + version: 5.0.6 '@types/express-serve-static-core': specifier: catalog:devTools version: 5.1.0 '@types/node': specifier: ^24.10.1 - version: 24.10.3 + version: 24.10.4 '@types/supertest': specifier: catalog:devTools version: 6.0.3 @@ -184,43 +184,43 @@ importers: version: 8.18.1 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20251218.3 + version: 7.0.0-dev.20260109.1 eslint: specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: catalog:devTools - version: 10.1.8(eslint@9.39.1) + version: 10.1.8(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools - version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) prettier: specifier: catalog:devTools version: 3.6.2 supertest: specifier: catalog:devTools - version: 7.1.4 + version: 7.2.2 tsdown: specifier: catalog:devTools - version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) tsx: specifier: catalog:devTools - version: 4.20.6 + version: 4.21.0 typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) ws: specifier: catalog:devTools - version: 8.18.3 + version: 8.19.0 zod: specifier: catalog:runtimeShared - version: 3.25.76 + version: 4.3.5 common/eslint-config: dependencies: @@ -230,31 +230,31 @@ importers: devDependencies: '@eslint/js': specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint: specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: catalog:devTools - version: 10.1.8(eslint@9.39.1) + version: 10.1.8(eslint@9.39.2) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1) + version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + version: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools - version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) eslint-plugin-simple-import-sort: specifier: ^12.1.1 - version: 12.1.1(eslint@9.39.1) + version: 12.1.1(eslint@9.39.2) prettier: specifier: catalog:devTools version: 3.6.2 typescript-eslint: specifier: catalog:devTools - version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) common/tsconfig: dependencies: @@ -273,7 +273,7 @@ importers: version: link:../tsconfig vite-tsconfig-paths: specifier: catalog:devTools - version: 5.1.4(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.3)(tsx@4.20.6)) + version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0)) examples/client: dependencies: @@ -285,7 +285,7 @@ importers: version: 8.17.1 zod: specifier: catalog:runtimeShared - version: 3.25.76 + version: 4.3.5 devDependencies: '@modelcontextprotocol/eslint-config': specifier: workspace:^ @@ -301,28 +301,28 @@ importers: version: link:../../common/vitest-config tsdown: specifier: catalog:devTools - version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) examples/server: dependencies: '@hono/node-server': specifier: catalog:runtimeServerOnly - version: 1.19.7(hono@4.11.1) + version: 1.19.8(hono@4.11.3) '@modelcontextprotocol/examples-shared': specifier: workspace:^ version: link:../shared - '@modelcontextprotocol/server': + '@modelcontextprotocol/express': specifier: workspace:^ - version: link:../../packages/server - '@modelcontextprotocol/server-express': + version: link:../../packages/middleware/express + '@modelcontextprotocol/hono': specifier: workspace:^ - version: link:../../packages/server-express - '@modelcontextprotocol/server-hono': + version: link:../../packages/middleware/hono + '@modelcontextprotocol/server': specifier: workspace:^ - version: link:../../packages/server-hono + version: link:../../packages/server better-auth: specifier: ^1.4.7 - version: 1.4.7(better-sqlite3@11.10.0)(vitest@4.0.9(@types/node@24.10.3)(tsx@4.20.6)) + version: 1.4.10(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)) cors: specifier: catalog:runtimeServerOnly version: 2.8.5 @@ -331,10 +331,10 @@ importers: version: 5.2.1 hono: specifier: catalog:runtimeServerOnly - version: 4.11.1 + version: 4.11.3 zod: specifier: catalog:runtimeShared - version: 3.25.76 + version: 4.3.5 devDependencies: '@modelcontextprotocol/eslint-config': specifier: workspace:^ @@ -350,35 +350,35 @@ importers: version: 2.8.19 '@types/express': specifier: catalog:devTools - version: 5.0.5 + version: 5.0.6 tsdown: specifier: catalog:devTools - version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) examples/shared: dependencies: '@modelcontextprotocol/core': specifier: workspace:^ version: link:../../packages/core + '@modelcontextprotocol/express': + specifier: workspace:^ + version: link:../../packages/middleware/express '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server - '@modelcontextprotocol/server-express': - specifier: workspace:^ - version: link:../../packages/server-express better-auth: specifier: ^1.4.7 - version: 1.4.7(better-sqlite3@11.10.0)(vitest@4.0.9(@types/node@24.10.3)(tsx@4.20.6)) + version: 1.4.10(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)) better-sqlite3: - specifier: ^11.10.0 - version: 11.10.0 + specifier: ^12.4.1 + version: 12.5.0 express: specifier: catalog:runtimeServerOnly version: 5.2.1 devDependencies: '@eslint/js': specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config @@ -396,34 +396,34 @@ importers: version: 7.6.13 '@types/express': specifier: catalog:devTools - version: 5.0.5 + version: 5.0.6 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20251218.3 + version: 7.0.0-dev.20260109.1 eslint: specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: catalog:devTools - version: 10.1.8(eslint@9.39.1) + version: 10.1.8(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools - version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) prettier: specifier: catalog:devTools version: 3.6.2 tsx: specifier: catalog:devTools - version: 4.20.6 + version: 4.21.0 typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) packages/client: dependencies: @@ -441,17 +441,17 @@ importers: version: 6.1.3 pkce-challenge: specifier: catalog:runtimeShared - version: 5.0.0 + version: 5.0.1 zod: specifier: catalog:runtimeShared - version: 3.25.76 + version: 4.3.5 devDependencies: '@cfworker/json-schema': specifier: catalog:runtimeShared version: 4.1.1 '@eslint/js': specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 '@modelcontextprotocol/core': specifier: workspace:^ version: link:../core @@ -478,34 +478,34 @@ importers: version: 1.1.15 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20251218.3 + version: 7.0.0-dev.20260109.1 eslint: specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: catalog:devTools - version: 10.1.8(eslint@9.39.1) + version: 10.1.8(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools - version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) prettier: specifier: catalog:devTools version: 3.6.2 tsdown: specifier: catalog:devTools - version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) tsx: specifier: catalog:devTools - version: 4.20.6 + version: 4.21.0 typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) packages/core: dependencies: @@ -520,17 +520,17 @@ importers: version: 8.0.2 zod: specifier: catalog:runtimeShared - version: 3.25.76 + version: 4.3.5 zod-to-json-schema: specifier: catalog:runtimeShared - version: 3.25.0(zod@3.25.76) + version: 3.25.1(zod@4.3.5) devDependencies: '@cfworker/json-schema': specifier: catalog:runtimeShared version: 4.1.1 '@eslint/js': specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config @@ -554,95 +554,65 @@ importers: version: 1.1.15 '@types/express': specifier: catalog:devTools - version: 5.0.5 + version: 5.0.6 '@types/express-serve-static-core': specifier: catalog:devTools version: 5.1.0 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20251218.3 + version: 7.0.0-dev.20260109.1 eslint: specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: catalog:devTools - version: 10.1.8(eslint@9.39.1) + version: 10.1.8(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools - version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) prettier: specifier: catalog:devTools version: 3.6.2 tsx: specifier: catalog:devTools - version: 4.20.6 + version: 4.21.0 typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) - packages/server: + packages/middleware/express: dependencies: - '@hono/node-server': - specifier: catalog:runtimeServerOnly - version: 1.19.7(hono@4.11.1) - content-type: + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../../server + '@remix-run/node-fetch-server': specifier: catalog:runtimeServerOnly - version: 1.0.5 - pkce-challenge: - specifier: catalog:runtimeShared - version: 5.0.0 - raw-body: + version: 0.13.0 + express: specifier: catalog:runtimeServerOnly - version: 3.0.1 - zod: - specifier: catalog:runtimeShared - version: 3.25.76 - zod-to-json-schema: - specifier: catalog:runtimeShared - version: 3.25.0(zod@3.25.76) + version: 5.2.1 devDependencies: - '@cfworker/json-schema': - specifier: catalog:runtimeShared - version: 4.1.1 '@eslint/js': specifier: catalog:devTools - version: 9.39.1 - '@modelcontextprotocol/core': - specifier: workspace:^ - version: link:../core + version: 9.39.2 '@modelcontextprotocol/eslint-config': specifier: workspace:^ - version: link:../../common/eslint-config - '@modelcontextprotocol/test-helpers': - specifier: workspace:^ - version: link:../../test/helpers + version: link:../../../common/eslint-config '@modelcontextprotocol/tsconfig': specifier: workspace:^ - version: link:../../common/tsconfig + version: link:../../../common/tsconfig '@modelcontextprotocol/vitest-config': specifier: workspace:^ - version: link:../../common/vitest-config - '@types/content-type': - specifier: catalog:devTools - version: 1.1.9 - '@types/cors': - specifier: catalog:devTools - version: 2.8.19 - '@types/cross-spawn': - specifier: catalog:devTools - version: 6.0.6 - '@types/eventsource': - specifier: catalog:devTools - version: 1.1.15 + version: link:../../../common/vitest-config '@types/express': specifier: catalog:devTools - version: 5.0.5 + version: 5.0.6 '@types/express-serve-static-core': specifier: catalog:devTools version: 5.1.0 @@ -651,65 +621,129 @@ importers: version: 6.0.3 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20251218.3 + version: 7.0.0-dev.20260109.1 eslint: specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: catalog:devTools - version: 10.1.8(eslint@9.39.1) + version: 10.1.8(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools - version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) prettier: specifier: catalog:devTools version: 3.6.2 supertest: specifier: catalog:devTools - version: 7.1.4 + version: 7.2.2 tsdown: specifier: catalog:devTools - version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) - tsx: - specifier: catalog:devTools - version: 4.20.6 + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) - packages/server-express: + packages/middleware/hono: dependencies: '@modelcontextprotocol/server': specifier: workspace:^ - version: link:../server - '@remix-run/node-fetch-server': + version: link:../../server + hono: specifier: catalog:runtimeServerOnly - version: 0.13.0 - express: + version: 4.11.3 + devDependencies: + '@eslint/js': + specifier: catalog:devTools + version: 9.39.2 + '@modelcontextprotocol/eslint-config': + specifier: workspace:^ + version: link:../../../common/eslint-config + '@modelcontextprotocol/tsconfig': + specifier: workspace:^ + version: link:../../../common/tsconfig + '@modelcontextprotocol/vitest-config': + specifier: workspace:^ + version: link:../../../common/vitest-config + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20260109.1 + eslint: + specifier: catalog:devTools + version: 9.39.2 + eslint-config-prettier: + specifier: catalog:devTools + version: 10.1.8(eslint@9.39.2) + eslint-plugin-n: + specifier: catalog:devTools + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) + prettier: + specifier: catalog:devTools + version: 3.6.2 + tsdown: + specifier: catalog:devTools + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + typescript: + specifier: catalog:devTools + version: 5.9.3 + typescript-eslint: + specifier: catalog:devTools + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + vitest: + specifier: catalog:devTools + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) + + packages/middleware/node: + dependencies: + '@hono/node-server': specifier: catalog:runtimeServerOnly - version: 5.2.1 + version: 1.19.8(hono@4.11.3) + content-type: + specifier: catalog:runtimeServerOnly + version: 1.0.5 + raw-body: + specifier: catalog:runtimeServerOnly + version: 3.0.2 devDependencies: '@eslint/js': specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 '@modelcontextprotocol/eslint-config': specifier: workspace:^ - version: link:../../common/eslint-config + version: link:../../../common/eslint-config + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../../server + '@modelcontextprotocol/test-helpers': + specifier: workspace:^ + version: link:../../../test/helpers '@modelcontextprotocol/tsconfig': specifier: workspace:^ - version: link:../../common/tsconfig + version: link:../../../common/tsconfig '@modelcontextprotocol/vitest-config': specifier: workspace:^ - version: link:../../common/vitest-config + version: link:../../../common/vitest-config + '@types/content-type': + specifier: catalog:devTools + version: 1.1.9 + '@types/cors': + specifier: catalog:devTools + version: 2.8.19 + '@types/cross-spawn': + specifier: catalog:devTools + version: 6.0.6 + '@types/eventsource': + specifier: catalog:devTools + version: 1.1.15 '@types/express': specifier: catalog:devTools - version: 5.0.5 + version: 5.0.6 '@types/express-serve-static-core': specifier: catalog:devTools version: 5.1.0 @@ -718,83 +752,107 @@ importers: version: 6.0.3 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20251218.3 + version: 7.0.0-dev.20260109.1 eslint: specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: catalog:devTools - version: 10.1.8(eslint@9.39.1) + version: 10.1.8(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools - version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) prettier: specifier: catalog:devTools version: 3.6.2 supertest: specifier: catalog:devTools - version: 7.1.4 + version: 7.2.2 tsdown: specifier: catalog:devTools - version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + tsx: + specifier: catalog:devTools + version: 4.21.0 typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) - packages/server-hono: + packages/server: dependencies: - '@modelcontextprotocol/server': - specifier: workspace:^ - version: link:../server - hono: - specifier: catalog:runtimeServerOnly - version: 4.11.1 + zod: + specifier: catalog:runtimeShared + version: 4.3.5 + zod-to-json-schema: + specifier: catalog:runtimeShared + version: 3.25.1(zod@4.3.5) devDependencies: + '@cfworker/json-schema': + specifier: catalog:runtimeShared + version: 4.1.1 '@eslint/js': specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 + '@modelcontextprotocol/core': + specifier: workspace:^ + version: link:../core '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config + '@modelcontextprotocol/test-helpers': + specifier: workspace:^ + version: link:../../test/helpers '@modelcontextprotocol/tsconfig': specifier: workspace:^ version: link:../../common/tsconfig '@modelcontextprotocol/vitest-config': specifier: workspace:^ version: link:../../common/vitest-config + '@types/cross-spawn': + specifier: catalog:devTools + version: 6.0.6 + '@types/eventsource': + specifier: catalog:devTools + version: 1.1.15 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20251218.3 + version: 7.0.0-dev.20260109.1 eslint: specifier: catalog:devTools - version: 9.39.1 + version: 9.39.2 eslint-config-prettier: specifier: catalog:devTools - version: 10.1.8(eslint@9.39.1) + version: 10.1.8(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools - version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) prettier: specifier: catalog:devTools version: 3.6.2 + supertest: + specifier: catalog:devTools + version: 7.2.2 tsdown: specifier: catalog:devTools - version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + tsx: + specifier: catalog:devTools + version: 4.21.0 typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) test/helpers: devDependencies: @@ -818,10 +876,10 @@ importers: version: link:../../common/vitest-config vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) zod: specifier: catalog:runtimeShared - version: 3.25.76 + version: 4.3.5 test/integration: devDependencies: @@ -834,12 +892,12 @@ importers: '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config + '@modelcontextprotocol/express': + specifier: workspace:^ + version: link:../../packages/middleware/express '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server - '@modelcontextprotocol/server-express': - specifier: workspace:^ - version: link:../../packages/server-express '@modelcontextprotocol/test-helpers': specifier: workspace:^ version: link:../helpers @@ -851,13 +909,13 @@ importers: version: link:../../common/vitest-config supertest: specifier: catalog:devTools - version: 7.1.4 + version: 7.2.2 vitest: specifier: catalog:devTools - version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) zod: specifier: catalog:runtimeShared - version: 3.25.76 + version: 4.3.5 packages: @@ -886,20 +944,20 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@better-auth/core@1.4.7': - resolution: {integrity: sha512-rNfj8aNFwPwAMYo+ahoWDsqKrV7svD3jhHSC6+A77xxKodbgV0UgH+RO21GMaZ0PPAibEl851nw5e3bsNslW/w==} + '@better-auth/core@1.4.10': + resolution: {integrity: sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg==} peerDependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 - better-call: 1.1.5 + better-call: 1.1.7 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 - '@better-auth/telemetry@1.4.7': - resolution: {integrity: sha512-k07C/FWnX6m+IxLruNkCweIxuaIwVTB2X40EqwamRVhYNBAhOYZFGLHH+PtQyM+Yf1Z4+8H6MugLOXSreXNAjQ==} + '@better-auth/telemetry@1.4.10': + resolution: {integrity: sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ==} peerDependencies: - '@better-auth/core': 1.4.7 + '@better-auth/core': 1.4.10 '@better-auth/utils@0.3.0': resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} @@ -971,173 +1029,173 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -1158,12 +1216,12 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.1': - resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -1174,8 +1232,8 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@hono/node-server@1.19.7': - resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==} + '@hono/node-server@1.19.8': + resolution: {integrity: sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 @@ -1241,8 +1299,8 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.1.0': - resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} '@noble/ciphers@2.1.1': resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} @@ -1268,8 +1326,11 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.101.0': - resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==} + '@oxc-project/types@0.103.0': + resolution: {integrity: sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==} + + '@oxc-project/types@0.107.0': + resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==} '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -1280,201 +1341,317 @@ packages: '@remix-run/node-fetch-server@0.13.0': resolution: {integrity: sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA==} - '@rolldown/binding-android-arm64@1.0.0-beta.53': - resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==} + '@rolldown/binding-android-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-beta.53': - resolution: {integrity: sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==} + '@rolldown/binding-android-arm64@1.0.0-beta.59': + resolution: {integrity: sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.59': + resolution: {integrity: sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.53': - resolution: {integrity: sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==} + '@rolldown/binding-darwin-x64@1.0.0-beta.57': + resolution: {integrity: sha512-6RsB8Qy4LnGqNGJJC/8uWeLWGOvbRL/KG5aJ8XXpSEupg/KQtlBEiFaYU/Ma5Usj1s+bt3ItkqZYAI50kSplBA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.53': - resolution: {integrity: sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==} + '@rolldown/binding-darwin-x64@1.0.0-beta.59': + resolution: {integrity: sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.57': + resolution: {integrity: sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': - resolution: {integrity: sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.59': + resolution: {integrity: sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': + resolution: {integrity: sha512-3KkS0cHsllT2T+Te+VZMKHNw6FPQihYsQh+8J4jkzwgvAQpbsbXmrqhkw3YU/QGRrD8qgcOvBr6z5y6Jid+rmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': - resolution: {integrity: sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': + resolution: {integrity: sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': + resolution: {integrity: sha512-A3/wu1RgsHhqP3rVH2+sM81bpk+Qd2XaHTl8LtX5/1LNR7QVBFBCpAoiXwjTdGnI5cMdBVi7Z1pi52euW760Fw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': + resolution: {integrity: sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': + resolution: {integrity: sha512-d0kIVezTQtazpyWjiJIn5to8JlwfKITDqwsFv0Xc6s31N16CD2PC/Pl2OtKgS7n8WLOJbfqgIp5ixYzTAxCqMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': - resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': + resolution: {integrity: sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': - resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': + resolution: {integrity: sha512-E199LPijo98yrLjPCmETx8EF43sZf9t3guSrLee/ej1rCCc3zDVTR4xFfN9BRAapGVl7/8hYqbbiQPTkv73kUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': - resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': + resolution: {integrity: sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': - resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': + resolution: {integrity: sha512-++EQDpk/UJ33kY/BNsh7A7/P1sr/jbMuQ8cE554ZIy+tCUWCivo9zfyjDUoiMdnxqX6HLJEqqGnbGQOvzm2OMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': + resolution: {integrity: sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': + resolution: {integrity: sha512-voDEBcNqxbUv/GeXKFtxXVWA+H45P/8Dec4Ii/SbyJyGvCqV1j+nNHfnFUIiRQ2Q40DwPe/djvgYBs9PpETiMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': + resolution: {integrity: sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': - resolution: {integrity: sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': + resolution: {integrity: sha512-bRhcF7NLlCnpkzLVlVhrDEd0KH22VbTPkPTbMjlYvqhSmarxNIq5vtlQS8qmV7LkPKHrNLWyJW/V/sOyFba26Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': + resolution: {integrity: sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': - resolution: {integrity: sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': + resolution: {integrity: sha512-rnDVGRks2FQ2hgJ2g15pHtfxqkGFGjJQUDWzYznEkE8Ra2+Vag9OffxdbJMZqBWXHVM0iS4dv8qSiEn7bO+n1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': + resolution: {integrity: sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': - resolution: {integrity: sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': + resolution: {integrity: sha512-OqIUyNid1M4xTj6VRXp/Lht/qIP8fo25QyAZlCP+p6D2ATCEhyW4ZIFLnC9zAGN/HMbXoCzvwfa8Jjg/8J4YEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': + resolution: {integrity: sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-beta.53': - resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rolldown/pluginutils@1.0.0-beta.57': + resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} - '@rollup/rollup-android-arm-eabi@4.53.2': - resolution: {integrity: sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==} + '@rolldown/pluginutils@1.0.0-beta.59': + resolution: {integrity: sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==} + + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.53.2': - resolution: {integrity: sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==} + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.53.2': - resolution: {integrity: sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==} + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.53.2': - resolution: {integrity: sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==} + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.53.2': - resolution: {integrity: sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==} + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.53.2': - resolution: {integrity: sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==} + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.53.2': - resolution: {integrity: sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==} + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.53.2': - resolution: {integrity: sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==} + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.53.2': - resolution: {integrity: sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==} + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.53.2': - resolution: {integrity: sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==} + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + cpu: [loong64] + os: [linux] + libc: [glibc] - '@rollup/rollup-linux-loong64-gnu@4.53.2': - resolution: {integrity: sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==} + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.53.2': - resolution: {integrity: sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==} + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.53.2': - resolution: {integrity: sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==} + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.53.2': - resolution: {integrity: sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==} + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.53.2': - resolution: {integrity: sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==} + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.53.2': - resolution: {integrity: sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==} + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.53.2': - resolution: {integrity: sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==} + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.53.2': - resolution: {integrity: sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==} + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.53.2': - resolution: {integrity: sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==} + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.53.2': - resolution: {integrity: sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==} + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.53.2': - resolution: {integrity: sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==} + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.53.2': - resolution: {integrity: sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==} + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} cpu: [x64] os: [win32] '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1515,8 +1692,8 @@ packages: '@types/express-serve-static-core@5.1.0': resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} - '@types/express@5.0.5': - resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -1530,14 +1707,11 @@ packages: '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@24.10.3': - resolution: {integrity: sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==} + '@types/node@24.10.4': + resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1545,14 +1719,11 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} '@types/superagent@8.1.9': resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} @@ -1563,102 +1734,102 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.49.0': - resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} + '@typescript-eslint/eslint-plugin@8.52.0': + resolution: {integrity: sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.49.0 + '@typescript-eslint/parser': ^8.52.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.49.0': - resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} + '@typescript-eslint/parser@8.52.0': + resolution: {integrity: sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.49.0': - resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} + '@typescript-eslint/project-service@8.52.0': + resolution: {integrity: sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.49.0': - resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} + '@typescript-eslint/scope-manager@8.52.0': + resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.49.0': - resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} + '@typescript-eslint/tsconfig-utils@8.52.0': + resolution: {integrity: sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.49.0': - resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} + '@typescript-eslint/type-utils@8.52.0': + resolution: {integrity: sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.49.0': - resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} + '@typescript-eslint/types@8.52.0': + resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.49.0': - resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} + '@typescript-eslint/typescript-estree@8.52.0': + resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.49.0': - resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} + '@typescript-eslint/utils@8.52.0': + resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.49.0': - resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} + '@typescript-eslint/visitor-keys@8.52.0': + resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251218.3': - resolution: {integrity: sha512-4Ew2waH0alZk3fuTIFJ7EsfjIqDj3mNYLa9aTQse6Xnfv16uFEwHbMs4RR1k6qPbPMnyKMC9fpY6f6p1WAPw4A==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260109.1': + resolution: {integrity: sha512-rEY7JFH9JhIQ7SCjD+cpwPhIBLzNOgA7IVkfIcOpbWTmtOufx0sTZejR5B2b81x2fLCJDPZGpUv71wD1LP45iA==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20251218.3': - resolution: {integrity: sha512-d+6aZW6ig5QdeZzGct6pm0/QoY+cDjUNggG34EefU8m13ThQUCYZ8xMxtSBJsPpFB7AX+iewE2DJtBdgoEbPdg==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260109.1': + resolution: {integrity: sha512-zBrxf4LYMhLGimvEZHJjtpYnpSqV4Q0rOkXEi8I5durn9NaGIBTOebBYXwF8/na6Pufdqd+vI1KQYxkm2G02pw==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20251218.3': - resolution: {integrity: sha512-0LY5RZTvShYYKyxy6ApDqpR0fMUaZUwpNYt9tRUoO4zfTLQdn38xMC4vgRZ3AtYSFX0Y185vLraa+tsdVJcvRw==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260109.1': + resolution: {integrity: sha512-WeDI+wrA1GqBFwFzj9i/zXlOuUaJdKidg6Jgry1P1TNpsHYW5YiKoNcpFirm1Fq3Dnav5cAa66z6VK5lvz7tgQ==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20251218.3': - resolution: {integrity: sha512-V62lMa3P4Tqynh2qdIEg9MA43Q19YoVbiG+Am6+TL3JV1gkPNCKr17risKz2GXuNRDJvaxrOm6OTRkceoK8Wgg==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260109.1': + resolution: {integrity: sha512-J8kHoVttxNeMq1wdT12HPe6i/524svbdw1RsMBgb+kbqTfFFElSK7rbeZFuTfzpEA0/c6y2MY96qLP07Fq4zEw==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20251218.3': - resolution: {integrity: sha512-Qi51LQVNiyKcMUnmx2rwSSBWjcxZ+Ec0nkpmeimV0WT1LZJ7ZipJ7KvRRcqLHF2WRk14kknFqDAS83RjdSkPfQ==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260109.1': + resolution: {integrity: sha512-D0nuBsJTIfc1JD2HyoMKqc2Wpe0tMAP92hgwap6E3iTlKFMW9ayd7KLUjLz6EFxxw9LRw2sjECT1VvjCjAJ4GQ==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20251218.3': - resolution: {integrity: sha512-y2EQoWU9CfVY3qBUbv0JJqddwsUrz7sRx+08mFg3XZOAMPfq6nwmsGrjNeiYBVwIg7gkZO4BqHL0Ktn6PZ9unA==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260109.1': + resolution: {integrity: sha512-nv365W1TiJEAAJ/NiBaKE9hSvG7U7ipuAoaSdr6HzdyMVouArq2DxxLo06mzDMOjNdc6U7vAIkLB52CcK3Y8ZQ==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20251218.3': - resolution: {integrity: sha512-X1s/tIpHhJP3OL/1qAXnfl8XUoCpv5DmWZhLcFB6F7ZjtH4EmvorrCaeSxdb88/KiL8gcxwipdm69eJb0fZWvA==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260109.1': + resolution: {integrity: sha512-da44CbC8ktr741ISLvCQlz3Gv2UqO2M+rB585xCFNjcz+0IyOKkBGr9eR++f6uy46/QKFK4w44x0cK71PVqk9g==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20251218.3': - resolution: {integrity: sha512-FxHW0n7KFG3QhcwRDsP/EWzkvjQYnIqfuDn1y0w4pl6KNh2aK9CRvHjA7bwpft64Tm5eCtYTHM+Gts7MElo9fA==} + '@typescript/native-preview@7.0.0-dev.20260109.1': + resolution: {integrity: sha512-27XQhOQWcGp7/nOS1NbEoC4vA2dZOmG5X+OP4e5KX2uAUc2cjE1Scn1Nnv9D7wU2ZBA+/wrqqvJqidCPFRlq+A==} hasBin: true '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -1700,41 +1871,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1756,11 +1935,11 @@ packages: cpu: [x64] os: [win32] - '@vitest/expect@4.0.9': - resolution: {integrity: sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==} + '@vitest/expect@4.0.16': + resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} - '@vitest/mocker@4.0.9': - resolution: {integrity: sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==} + '@vitest/mocker@4.0.16': + resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0-0 @@ -1770,20 +1949,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.0.9': - resolution: {integrity: sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==} + '@vitest/pretty-format@4.0.16': + resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} - '@vitest/runner@4.0.9': - resolution: {integrity: sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==} + '@vitest/runner@4.0.16': + resolution: {integrity: sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==} - '@vitest/snapshot@4.0.9': - resolution: {integrity: sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==} + '@vitest/snapshot@4.0.16': + resolution: {integrity: sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==} - '@vitest/spy@4.0.9': - resolution: {integrity: sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==} + '@vitest/spy@4.0.16': + resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==} - '@vitest/utils@4.0.9': - resolution: {integrity: sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==} + '@vitest/utils@4.0.16': + resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -1891,26 +2070,26 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-auth@1.4.7: - resolution: {integrity: sha512-kVmDQxzqGwP4FFMOYpS5I7oAaoFW3hwooUAAtcbb2DrOYv5EUvRUDJbTMaPoMTj7URjNDQ6vG9gcCS1Q+0aVBw==} + better-auth@1.4.10: + resolution: {integrity: sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg==} peerDependencies: '@lynx-js/react': '*' - '@prisma/client': ^5.22.0 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 '@sveltejs/kit': ^2.0.0 '@tanstack/react-start': ^1.0.0 - better-sqlite3: ^12.4.1 - drizzle-kit: ^0.31.4 - drizzle-orm: ^0.41.0 - mongodb: ^6.18.0 - mysql2: ^3.14.4 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 next: ^14.0.0 || ^15.0.0 || ^16.0.0 - pg: ^8.16.3 - prisma: ^5.22.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 - vitest: ^4.0.15 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': @@ -1950,8 +2129,8 @@ packages: vue: optional: true - better-call@1.1.5: - resolution: {integrity: sha512-nQJ3S87v6wApbDwbZ++FrQiSiVxWvZdjaO+2v6lZJAG2WWggkB2CziUDjPciz3eAt9TqfRursIQMZIcpkBnvlw==} + better-call@1.1.7: + resolution: {integrity: sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -1962,20 +2141,21 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - better-sqlite3@11.10.0: - resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + better-sqlite3@12.5.0: + resolution: {integrity: sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - birpc@3.0.0: - resolution: {integrity: sha512-by+04pHuxpCEQcucAXqzopqfhyI8TLK5Qg5MST0cB6MP+JhHna9ollrtK9moVh27aq6Q6MEJgebD0cVm//yBkg==} + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@2.2.1: - resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} brace-expansion@1.1.12: @@ -2015,8 +2195,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - chai@6.2.1: - resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} chalk@4.1.2: @@ -2054,9 +2234,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} @@ -2193,16 +2373,16 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.18.3: - resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -2232,8 +2412,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} hasBin: true @@ -2341,8 +2521,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.1: - resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2360,8 +2540,8 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -2395,8 +2575,8 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} express-rate-limit@7.5.1: @@ -2431,8 +2611,8 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -2454,9 +2634,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.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==} @@ -2477,8 +2657,8 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} formidable@3.5.4: @@ -2602,23 +2782,23 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hono@4.11.1: - resolution: {integrity: sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==} + hono@4.11.3: + resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==} engines: {node: '>=16.9.0'} - hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hookable@6.0.1: + resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true - iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -2636,8 +2816,8 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-without-cache@0.2.3: - resolution: {integrity: sha512-roCvX171VqJ7+7pQt1kSRfwaJvFAC2zhThJWXal1rN8EqzPS3iapkAoNpHh4lM8Na1BDen+n9rVfo73RN+Y87g==} + import-without-cache@0.2.5: + resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} engines: {node: '>=20.19.0'} imurmurhash@0.1.4: @@ -2880,9 +3060,9 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} @@ -3074,8 +3254,8 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - pkce-challenge@5.0.0: - resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} possible-typed-array-names@1.1.0: @@ -3116,8 +3296,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -3133,8 +3313,8 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.1: - resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} rc@1.2.8: @@ -3181,15 +3361,15 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown-plugin-dts@0.18.3: - resolution: {integrity: sha512-rd1LZ0Awwfyn89UndUF/HoFF4oH9a5j+2ZeuKSJYM80vmeN/p0gslYMnHTQHBEXPhUlvAlqGA3tVgXB/1qFNDg==} + rolldown-plugin-dts@0.20.0: + resolution: {integrity: sha512-cLAY1kN2ilTYMfZcFlGWbXnu6Nb+8uwUBsi+Mjbh4uIx7IN8uMOmJ7RxrrRgPsO4H7eSz3E+JwGoL1gyugiyUA==} engines: {node: '>=20.19.0'} peerDependencies: '@ts-macro/tsc': ^0.3.6 '@typescript/native-preview': '>=7.0.0-dev.20250601.1' - rolldown: ^1.0.0-beta.51 + rolldown: ^1.0.0-beta.57 typescript: ^5.0.0 - vue-tsc: ~3.1.0 + vue-tsc: ~3.2.0 peerDependenciesMeta: '@ts-macro/tsc': optional: true @@ -3200,13 +3380,18 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-beta.53: - resolution: {integrity: sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==} + rolldown@1.0.0-beta.57: + resolution: {integrity: sha512-lMMxcNN71GMsSko8RyeTaFoATHkCh4IWU7pYF73ziMYjhHZWfVesC6GQ+iaJCvZmVjvgSks9Ks1aaqEkBd8udg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.53.2: - resolution: {integrity: sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==} + rolldown@1.0.0-beta.59: + resolution: {integrity: sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3247,12 +3432,12 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} set-cookie-parser@2.7.2: @@ -3331,10 +3516,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -3377,12 +3558,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - superagent@10.2.3: - resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} - supertest@7.1.4: - resolution: {integrity: sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==} + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} engines: {node: '>=14.18.0'} supports-color@7.2.0: @@ -3411,9 +3592,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -3441,8 +3619,8 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -3465,13 +3643,13 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tsdown@0.18.0: - resolution: {integrity: sha512-Yotdh3NzizysnqR96xfpHFYtEntk1cZvSRHz8A+Pn3ZHNdTQa4fBQxh6HHzWZwfjdQv47xb7GCv6vEWMtxBirw==} + tsdown@0.18.4: + resolution: {integrity: sha512-J/tRS6hsZTkvqmt4+xdELUCkQYDuUCXgBv0fw3ImV09WPGbEKfsPD65E+WUjSu3E7Z6tji9XZ1iWs8rbGqB/ZA==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@vitejs/devtools': ^0.0.0-alpha.19 + '@vitejs/devtools': '*' publint: ^0.3.0 typescript: ^5.0.0 unplugin-lightningcss: ^0.4.0 @@ -3493,8 +3671,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.20.6: - resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} hasBin: true @@ -3525,8 +3703,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.49.0: - resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} + typescript-eslint@8.52.0: + resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3558,8 +3736,8 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unrun@0.2.19: - resolution: {integrity: sha512-DbwbJ9BvPEb3BeZnIpP9S5tGLO/JIgPQ3JrpMRFIfZMZfMG19f26OlLbC2ml8RRdrI2ZA7z2t+at5tsIHbh6Qw==} + unrun@0.2.24: + resolution: {integrity: sha512-xa4/O5q2jmI6EqxweJ+sOy5cyORZWcsgmi8pmABVSUyg24Fh44qJrneUHavZEMsbJbghHYWKSraFy5hDCb/m4w==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: @@ -3586,8 +3764,8 @@ packages: vite: optional: true - vite@7.2.2: - resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3626,24 +3804,24 @@ packages: yaml: optional: true - vitest@4.0.9: - resolution: {integrity: sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==} + vitest@4.0.16: + resolution: {integrity: sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 + '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.9 - '@vitest/browser-preview': 4.0.9 - '@vitest/browser-webdriverio': 4.0.9 - '@vitest/ui': 4.0.9 + '@vitest/browser-playwright': 4.0.16 + '@vitest/browser-preview': 4.0.16 + '@vitest/browser-webdriverio': 4.0.16 + '@vitest/ui': 4.0.16 happy-dom: '*' jsdom: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@types/debug': + '@opentelemetry/api': optional: true '@types/node': optional: true @@ -3699,8 +3877,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3715,16 +3893,16 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod-to-json-schema@3.25.0: - resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: zod: ^3.25 || ^4 zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.2.1: - resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} snapshots: @@ -3751,20 +3929,20 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)': + '@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 - '@standard-schema/spec': 1.0.0 - better-call: 1.1.5(zod@4.2.1) + '@standard-schema/spec': 1.1.0 + better-call: 1.1.7(zod@4.3.5) jose: 6.1.3 kysely: 0.28.9 nanostores: 1.1.0 - zod: 4.2.1 + zod: 4.3.5 - '@better-auth/telemetry@1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))': + '@better-auth/telemetry@1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))': dependencies: - '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -3811,7 +3989,7 @@ snapshots: transitivePeerDependencies: - encoding - '@changesets/cli@2.29.8(@types/node@24.10.3)': + '@changesets/cli@2.29.8(@types/node@24.10.4)': dependencies: '@changesets/apply-release-plan': 7.0.14 '@changesets/assemble-release-plan': 6.0.9 @@ -3827,7 +4005,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@24.10.3) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.4) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 @@ -3933,13 +4111,13 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@emnapi/core@1.7.1': + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.1': + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true @@ -3949,87 +4127,87 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.12': + '@esbuild/aix-ppc64@0.27.2': optional: true - '@esbuild/android-arm64@0.25.12': + '@esbuild/android-arm64@0.27.2': optional: true - '@esbuild/android-arm@0.25.12': + '@esbuild/android-arm@0.27.2': optional: true - '@esbuild/android-x64@0.25.12': + '@esbuild/android-x64@0.27.2': optional: true - '@esbuild/darwin-arm64@0.25.12': + '@esbuild/darwin-arm64@0.27.2': optional: true - '@esbuild/darwin-x64@0.25.12': + '@esbuild/darwin-x64@0.27.2': optional: true - '@esbuild/freebsd-arm64@0.25.12': + '@esbuild/freebsd-arm64@0.27.2': optional: true - '@esbuild/freebsd-x64@0.25.12': + '@esbuild/freebsd-x64@0.27.2': optional: true - '@esbuild/linux-arm64@0.25.12': + '@esbuild/linux-arm64@0.27.2': optional: true - '@esbuild/linux-arm@0.25.12': + '@esbuild/linux-arm@0.27.2': optional: true - '@esbuild/linux-ia32@0.25.12': + '@esbuild/linux-ia32@0.27.2': optional: true - '@esbuild/linux-loong64@0.25.12': + '@esbuild/linux-loong64@0.27.2': optional: true - '@esbuild/linux-mips64el@0.25.12': + '@esbuild/linux-mips64el@0.27.2': optional: true - '@esbuild/linux-ppc64@0.25.12': + '@esbuild/linux-ppc64@0.27.2': optional: true - '@esbuild/linux-riscv64@0.25.12': + '@esbuild/linux-riscv64@0.27.2': optional: true - '@esbuild/linux-s390x@0.25.12': + '@esbuild/linux-s390x@0.27.2': optional: true - '@esbuild/linux-x64@0.25.12': + '@esbuild/linux-x64@0.27.2': optional: true - '@esbuild/netbsd-arm64@0.25.12': + '@esbuild/netbsd-arm64@0.27.2': optional: true - '@esbuild/netbsd-x64@0.25.12': + '@esbuild/netbsd-x64@0.27.2': optional: true - '@esbuild/openbsd-arm64@0.25.12': + '@esbuild/openbsd-arm64@0.27.2': optional: true - '@esbuild/openbsd-x64@0.25.12': + '@esbuild/openbsd-x64@0.27.2': optional: true - '@esbuild/openharmony-arm64@0.25.12': + '@esbuild/openharmony-arm64@0.27.2': optional: true - '@esbuild/sunos-x64@0.25.12': + '@esbuild/sunos-x64@0.27.2': optional: true - '@esbuild/win32-arm64@0.25.12': + '@esbuild/win32-arm64@0.27.2': optional: true - '@esbuild/win32-ia32@0.25.12': + '@esbuild/win32-ia32@0.27.2': optional: true - '@esbuild/win32-x64@0.25.12': + '@esbuild/win32-x64@0.27.2': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': dependencies: - eslint: 9.39.1 + eslint: 9.39.2 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -4050,7 +4228,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 debug: 4.4.3 @@ -4064,7 +4242,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.1': {} + '@eslint/js@9.39.2': {} '@eslint/object-schema@2.1.7': {} @@ -4073,9 +4251,9 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@hono/node-server@1.19.7(hono@4.11.1)': + '@hono/node-server@1.19.8(hono@4.11.3)': dependencies: - hono: 4.11.1 + hono: 4.11.3 '@humanfs/core@0.19.1': {} @@ -4088,12 +4266,12 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/external-editor@1.0.3(@types/node@24.10.3)': + '@inquirer/external-editor@1.0.3(@types/node@24.10.4)': dependencies: chardet: 2.1.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4125,9 +4303,9 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@modelcontextprotocol/conformance@0.1.9(@cfworker/json-schema@4.1.1)(hono@4.11.1)': + '@modelcontextprotocol/conformance@0.1.9(@cfworker/json-schema@4.1.1)(hono@4.11.3)': dependencies: - '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.1)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.3)(zod@3.25.76) commander: 14.0.2 eventsource-parser: 3.0.6 express: 5.2.1 @@ -4138,9 +4316,9 @@ snapshots: - hono - supports-color - '@modelcontextprotocol/sdk@1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.1)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.3)(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.7(hono@4.11.1) + '@hono/node-server': 1.19.8(hono@4.11.3) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -4152,10 +4330,10 @@ snapshots: express-rate-limit: 7.5.1(express@5.2.1) jose: 6.1.3 json-schema-typed: 8.0.2 - pkce-challenge: 5.0.0 - raw-body: 3.0.1 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-json-schema: 3.25.1(zod@3.25.76) optionalDependencies: '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: @@ -4164,15 +4342,15 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.1.0': + '@napi-rs/wasm-runtime@1.1.1': dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -4192,9 +4370,11 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 - '@oxc-project/types@0.101.0': {} + '@oxc-project/types@0.103.0': {} + + '@oxc-project/types@0.107.0': {} '@paralleldrive/cuid2@2.3.1': dependencies: @@ -4206,118 +4386,170 @@ snapshots: '@remix-run/node-fetch-server@0.13.0': {} - '@rolldown/binding-android-arm64@1.0.0-beta.53': + '@rolldown/binding-android-arm64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-android-arm64@1.0.0-beta.59': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.59': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.59': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.57': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.59': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.53': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.53': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.53': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': dependencies: - '@napi-rs/wasm-runtime': 1.1.0 + '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': optional: true - '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': + optional: true - '@rollup/rollup-android-arm-eabi@4.53.2': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': optional: true - '@rollup/rollup-android-arm64@4.53.2': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': optional: true - '@rollup/rollup-darwin-arm64@4.53.2': + '@rolldown/pluginutils@1.0.0-beta.57': {} + + '@rolldown/pluginutils@1.0.0-beta.59': {} + + '@rollup/rollup-android-arm-eabi@4.55.1': + optional: true + + '@rollup/rollup-android-arm64@4.55.1': optional: true - '@rollup/rollup-darwin-x64@4.53.2': + '@rollup/rollup-darwin-arm64@4.55.1': optional: true - '@rollup/rollup-freebsd-arm64@4.53.2': + '@rollup/rollup-darwin-x64@4.55.1': optional: true - '@rollup/rollup-freebsd-x64@4.53.2': + '@rollup/rollup-freebsd-arm64@4.55.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.53.2': + '@rollup/rollup-freebsd-x64@4.55.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.53.2': + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.53.2': + '@rollup/rollup-linux-arm-musleabihf@4.55.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.53.2': + '@rollup/rollup-linux-arm64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.53.2': + '@rollup/rollup-linux-arm64-musl@4.55.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.53.2': + '@rollup/rollup-linux-loong64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.53.2': + '@rollup/rollup-linux-loong64-musl@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.53.2': + '@rollup/rollup-linux-ppc64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.53.2': + '@rollup/rollup-linux-ppc64-musl@4.55.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.53.2': + '@rollup/rollup-linux-riscv64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-x64-musl@4.53.2': + '@rollup/rollup-linux-riscv64-musl@4.55.1': optional: true - '@rollup/rollup-openharmony-arm64@4.53.2': + '@rollup/rollup-linux-s390x-gnu@4.55.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.53.2': + '@rollup/rollup-linux-x64-gnu@4.55.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.53.2': + '@rollup/rollup-linux-x64-musl@4.55.1': optional: true - '@rollup/rollup-win32-x64-gnu@4.53.2': + '@rollup/rollup-openbsd-x64@4.55.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.53.2': + '@rollup/rollup-openharmony-arm64@4.55.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.1': optional: true '@rtsao/scc@1.1.0': {} - '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} '@tybys/wasm-util@0.10.1': dependencies: @@ -4326,12 +4558,12 @@ snapshots: '@types/better-sqlite3@7.6.13': dependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.10.3 + '@types/node': 24.10.4 '@types/chai@5.2.3': dependencies: @@ -4340,7 +4572,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 '@types/content-type@1.1.9': {} @@ -4348,11 +4580,11 @@ snapshots: '@types/cors@2.8.19': dependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 '@types/cross-spawn@6.0.6': dependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 '@types/deep-eql@4.0.2': {} @@ -4362,16 +4594,16 @@ snapshots: '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 - '@types/express@5.0.5': + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 '@types/express-serve-static-core': 5.1.0 - '@types/serve-static': 1.15.10 + '@types/serve-static': 2.2.0 '@types/http-errors@2.0.5': {} @@ -4381,11 +4613,9 @@ snapshots: '@types/methods@1.1.4': {} - '@types/mime@1.3.5': {} - '@types/node@12.20.55': {} - '@types/node@24.10.3': + '@types/node@24.10.4': dependencies: undici-types: 7.16.0 @@ -4393,27 +4623,21 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 24.10.3 - '@types/send@1.2.1': dependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 - '@types/serve-static@1.15.10': + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.3 - '@types/send': 0.17.6 + '@types/node': 24.10.4 '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 24.10.3 - form-data: 4.0.4 + '@types/node': 24.10.4 + form-data: 4.0.5 '@types/supertest@6.0.3': dependencies: @@ -4422,129 +4646,129 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 - eslint: 9.39.1 + '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.52.0 + '@typescript-eslint/type-utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.52.0 + eslint: 9.39.2 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/scope-manager': 8.52.0 + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.52.0 debug: 4.4.3 - eslint: 9.39.1 + eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.52.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.49.0': + '@typescript-eslint/scope-manager@8.52.0': dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/visitor-keys': 8.52.0 - '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.52.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.52.0(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.1 - ts-api-utils: 2.1.0(typescript@5.9.3) + eslint: 9.39.2 + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.49.0': {} + '@typescript-eslint/types@8.52.0': {} - '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.52.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/project-service': 8.52.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/visitor-keys': 8.52.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.49.0(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.52.0(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - eslint: 9.39.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@typescript-eslint/scope-manager': 8.52.0 + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.49.0': + '@typescript-eslint/visitor-keys@8.52.0': dependencies: - '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/types': 8.52.0 eslint-visitor-keys: 4.2.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251218.3': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260109.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20251218.3': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260109.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20251218.3': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260109.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20251218.3': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260109.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20251218.3': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260109.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20251218.3': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260109.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20251218.3': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260109.1': optional: true - '@typescript/native-preview@7.0.0-dev.20251218.3': + '@typescript/native-preview@7.0.0-dev.20260109.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20251218.3 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20251218.3 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20251218.3 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20251218.3 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20251218.3 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20251218.3 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20251218.3 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260109.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260109.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260109.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260109.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260109.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260109.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260109.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -4605,48 +4829,48 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/expect@4.0.9': + '@vitest/expect@4.0.16': dependencies: - '@standard-schema/spec': 1.0.0 + '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.9 - '@vitest/utils': 4.0.9 - chai: 6.2.1 + '@vitest/spy': 4.0.16 + '@vitest/utils': 4.0.16 + chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.9(vite@7.2.2(@types/node@24.10.3)(tsx@4.20.6))': + '@vitest/mocker@4.0.16(vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0))': dependencies: - '@vitest/spy': 4.0.9 + '@vitest/spy': 4.0.16 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.2(@types/node@24.10.3)(tsx@4.20.6) + vite: 7.3.1(@types/node@24.10.4)(tsx@4.21.0) - '@vitest/pretty-format@4.0.9': + '@vitest/pretty-format@4.0.16': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.9': + '@vitest/runner@4.0.16': dependencies: - '@vitest/utils': 4.0.9 + '@vitest/utils': 4.0.16 pathe: 2.0.3 - '@vitest/snapshot@4.0.9': + '@vitest/snapshot@4.0.16': dependencies: - '@vitest/pretty-format': 4.0.9 + '@vitest/pretty-format': 4.0.16 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.9': {} + '@vitest/spy@4.0.16': {} - '@vitest/utils@4.0.9': + '@vitest/utils@4.0.16': dependencies: - '@vitest/pretty-format': 4.0.9 + '@vitest/pretty-format': 4.0.16 tinyrainbow: 3.0.3 accepts@2.0.0: dependencies: - mime-types: 3.0.1 + mime-types: 3.0.2 negotiator: 1.0.0 acorn-jsx@5.3.2(acorn@8.15.0): @@ -4699,7 +4923,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 is-string: 1.1.1 @@ -4712,7 +4936,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -4721,14 +4945,14 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: @@ -4736,7 +4960,7 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -4762,38 +4986,38 @@ snapshots: base64-js@1.5.1: {} - better-auth@1.4.7(better-sqlite3@11.10.0)(vitest@4.0.9(@types/node@24.10.3)(tsx@4.20.6)): + better-auth@1.4.10(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)): dependencies: - '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) + '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 - better-call: 1.1.5(zod@4.2.1) + better-call: 1.1.7(zod@4.3.5) defu: 6.1.4 jose: 6.1.3 kysely: 0.28.9 nanostores: 1.1.0 - zod: 4.2.1 + zod: 4.3.5 optionalDependencies: - better-sqlite3: 11.10.0 - vitest: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + better-sqlite3: 12.5.0 + vitest: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) - better-call@1.1.5(zod@4.2.1): + better-call@1.1.7(zod@4.3.5): dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 rou3: 0.7.12 set-cookie-parser: 2.7.2 optionalDependencies: - zod: 4.2.1 + zod: 4.3.5 better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 - better-sqlite3@11.10.0: + better-sqlite3@12.5.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 @@ -4802,7 +5026,7 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 - birpc@3.0.0: {} + birpc@4.0.0: {} bl@4.1.0: dependencies: @@ -4810,16 +5034,16 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@2.2.1: + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 - http-errors: 2.0.0 - iconv-lite: 0.7.0 + http-errors: 2.0.1 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.1 + qs: 6.14.1 + raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -4865,7 +5089,7 @@ snapshots: callsites@3.1.0: {} - chai@6.2.1: {} + chai@6.2.2: {} chalk@4.1.2: dependencies: @@ -4894,9 +5118,7 @@ snapshots: concat-map@0.0.1: {} - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 + content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -5008,7 +5230,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.18.3: + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -5018,7 +5240,7 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - es-abstract@1.24.0: + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -5102,47 +5324,47 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.12: + esbuild@0.27.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.39.1): + eslint-compat-utils@0.5.1(eslint@9.39.2): dependencies: - eslint: 9.39.1 + eslint: 9.39.2 semver: 7.7.3 - eslint-config-prettier@10.1.8(eslint@9.39.1): + eslint-config-prettier@10.1.8(eslint@9.39.2): dependencies: - eslint: 9.39.1 + eslint: 9.39.2 eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: @@ -5159,10 +5381,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2): dependencies: debug: 4.4.3 - eslint: 9.39.1 + eslint: 9.39.2 eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.13.0 is-bun-module: 2.0.0 @@ -5170,29 +5392,29 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) - eslint: 9.39.1 + '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + eslint: 9.39.2 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.1) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2) transitivePeerDependencies: - supports-color - eslint-plugin-es-x@7.8.0(eslint@9.39.1): + eslint-plugin-es-x@7.8.0(eslint@9.39.2): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) '@eslint-community/regexpp': 4.12.2 - eslint: 9.39.1 - eslint-compat-utils: 0.5.1(eslint@9.39.1) + eslint: 9.39.2 + eslint-compat-utils: 0.5.1(eslint@9.39.2) - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5201,9 +5423,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1 + eslint: 9.39.2 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5215,18 +5437,18 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-n@17.23.1(eslint@9.39.1)(typescript@5.9.3): + eslint-plugin-n@17.23.1(eslint@9.39.2)(typescript@5.9.3): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) - enhanced-resolve: 5.18.3 - eslint: 9.39.1 - eslint-plugin-es-x: 7.8.0(eslint@9.39.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + enhanced-resolve: 5.18.4 + eslint: 9.39.2 + eslint-plugin-es-x: 7.8.0(eslint@9.39.2) get-tsconfig: 4.13.0 globals: 15.15.0 globrex: 0.1.2 @@ -5236,9 +5458,9 @@ snapshots: transitivePeerDependencies: - typescript - eslint-plugin-simple-import-sort@12.1.1(eslint@9.39.1): + eslint-plugin-simple-import-sort@12.1.1(eslint@9.39.2): dependencies: - eslint: 9.39.1 + eslint: 9.39.2 eslint-scope@8.4.0: dependencies: @@ -5249,15 +5471,15 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.1: + eslint@9.39.2: dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.1 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 @@ -5271,7 +5493,7 @@ snapshots: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - esquery: 1.6.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -5296,7 +5518,7 @@ snapshots: esprima@4.0.1: {} - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -5322,7 +5544,7 @@ snapshots: expand-template@2.0.3: {} - expect-type@1.2.2: {} + expect-type@1.3.0: {} express-rate-limit@7.5.1(express@5.2.1): dependencies: @@ -5331,8 +5553,8 @@ snapshots: express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.1 - content-disposition: 1.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 @@ -5341,20 +5563,20 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 + 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.1 + 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.14.0 + qs: 6.14.1 range-parser: 1.2.1 router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 @@ -5381,7 +5603,7 @@ snapshots: fast-uri@3.1.0: {} - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -5399,7 +5621,7 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: + finalhandler@2.1.1: dependencies: debug: 4.4.3 encodeurl: 2.0.0 @@ -5431,7 +5653,7 @@ snapshots: dependencies: is-callable: 1.2.7 - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -5565,21 +5787,21 @@ snapshots: dependencies: function-bind: 1.1.2 - hono@4.11.1: {} + hono@4.11.3: {} - hookable@5.5.3: {} + hookable@6.0.1: {} - http-errors@2.0.0: + http-errors@2.0.1: dependencies: depd: 2.0.0 inherits: 2.0.4 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 toidentifier: 1.0.1 human-id@4.1.3: {} - iconv-lite@0.7.0: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -5594,7 +5816,7 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-without-cache@0.2.3: {} + import-without-cache@0.2.5: {} imurmurhash@0.1.4: {} @@ -5815,7 +6037,7 @@ snapshots: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -5878,14 +6100,14 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 object.groupby@1.0.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 object.values@1.2.1: dependencies: @@ -5975,7 +6197,7 @@ snapshots: pify@4.0.1: {} - pkce-challenge@5.0.0: {} + pkce-challenge@5.0.1: {} possible-typed-array-names@1.1.0: {} @@ -6018,7 +6240,7 @@ snapshots: punycode@2.3.1: {} - qs@6.14.0: + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -6030,11 +6252,11 @@ snapshots: range-parser@1.2.1: {} - raw-body@3.0.1: + raw-body@3.0.2: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.7.0 + http-errors: 2.0.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 rc@1.2.8: @@ -6061,7 +6283,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -6093,69 +6315,90 @@ snapshots: reusify@1.1.0: {} - rolldown-plugin-dts@0.18.3(@typescript/native-preview@7.0.0-dev.20251218.3)(rolldown@1.0.0-beta.53)(typescript@5.9.3): + rolldown-plugin-dts@0.20.0(@typescript/native-preview@7.0.0-dev.20260109.1)(rolldown@1.0.0-beta.57)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 ast-kit: 2.2.0 - birpc: 3.0.0 + birpc: 4.0.0 dts-resolver: 2.1.3 get-tsconfig: 4.13.0 - magic-string: 0.30.21 obug: 2.1.1 - rolldown: 1.0.0-beta.53 + rolldown: 1.0.0-beta.57 optionalDependencies: - '@typescript/native-preview': 7.0.0-dev.20251218.3 + '@typescript/native-preview': 7.0.0-dev.20260109.1 typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver - rolldown@1.0.0-beta.53: + rolldown@1.0.0-beta.57: dependencies: - '@oxc-project/types': 0.101.0 - '@rolldown/pluginutils': 1.0.0-beta.53 + '@oxc-project/types': 0.103.0 + '@rolldown/pluginutils': 1.0.0-beta.57 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.57 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.57 + '@rolldown/binding-darwin-x64': 1.0.0-beta.57 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.57 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.57 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.57 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.57 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.57 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.57 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.57 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.57 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.57 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.57 + + rolldown@1.0.0-beta.59: + dependencies: + '@oxc-project/types': 0.107.0 + '@rolldown/pluginutils': 1.0.0-beta.59 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.53 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.53 - '@rolldown/binding-darwin-x64': 1.0.0-beta.53 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.53 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.53 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.53 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.53 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.53 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.53 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.53 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.53 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.53 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.53 - - rollup@4.53.2: + '@rolldown/binding-android-arm64': 1.0.0-beta.59 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.59 + '@rolldown/binding-darwin-x64': 1.0.0-beta.59 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.59 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.59 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.59 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.59 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.59 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.59 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.59 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.59 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.59 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.59 + + rollup@4.55.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.53.2 - '@rollup/rollup-android-arm64': 4.53.2 - '@rollup/rollup-darwin-arm64': 4.53.2 - '@rollup/rollup-darwin-x64': 4.53.2 - '@rollup/rollup-freebsd-arm64': 4.53.2 - '@rollup/rollup-freebsd-x64': 4.53.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.2 - '@rollup/rollup-linux-arm-musleabihf': 4.53.2 - '@rollup/rollup-linux-arm64-gnu': 4.53.2 - '@rollup/rollup-linux-arm64-musl': 4.53.2 - '@rollup/rollup-linux-loong64-gnu': 4.53.2 - '@rollup/rollup-linux-ppc64-gnu': 4.53.2 - '@rollup/rollup-linux-riscv64-gnu': 4.53.2 - '@rollup/rollup-linux-riscv64-musl': 4.53.2 - '@rollup/rollup-linux-s390x-gnu': 4.53.2 - '@rollup/rollup-linux-x64-gnu': 4.53.2 - '@rollup/rollup-linux-x64-musl': 4.53.2 - '@rollup/rollup-openharmony-arm64': 4.53.2 - '@rollup/rollup-win32-arm64-msvc': 4.53.2 - '@rollup/rollup-win32-ia32-msvc': 4.53.2 - '@rollup/rollup-win32-x64-gnu': 4.53.2 - '@rollup/rollup-win32-x64-msvc': 4.53.2 + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 fsevents: 2.3.3 rou3@0.7.12: {} @@ -6201,15 +6444,15 @@ snapshots: semver@7.7.3: {} - send@1.2.0: + 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.0 - mime-types: 3.0.1 + http-errors: 2.0.1 + mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -6217,12 +6460,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.0: + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 1.2.1 transitivePeerDependencies: - supports-color @@ -6313,8 +6556,6 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.1: {} - statuses@2.0.2: {} std-env@3.10.0: {} @@ -6330,7 +6571,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -6361,24 +6602,25 @@ snapshots: strip-json-comments@3.1.1: {} - superagent@10.2.3: + superagent@10.3.0: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 debug: 4.4.3 fast-safe-stringify: 2.1.1 - form-data: 4.0.4 + form-data: 4.0.5 formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.14.0 + qs: 6.14.1 transitivePeerDependencies: - supports-color - supertest@7.1.4: + supertest@7.2.2: dependencies: + cookie-signature: 1.2.2 methods: 1.1.2 - superagent: 10.2.3 + superagent: 10.3.0 transitivePeerDependencies: - supports-color @@ -6409,8 +6651,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -6430,7 +6670,7 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.1.0(typescript@5.9.3): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -6450,23 +6690,24 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsdown@0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3): + tsdown@0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3): dependencies: ansis: 4.2.0 cac: 6.7.14 defu: 6.1.4 empathic: 2.0.0 - hookable: 5.5.3 - import-without-cache: 0.2.3 + hookable: 6.0.1 + import-without-cache: 0.2.5 obug: 2.1.1 - rolldown: 1.0.0-beta.53 - rolldown-plugin-dts: 0.18.3(@typescript/native-preview@7.0.0-dev.20251218.3)(rolldown@1.0.0-beta.53)(typescript@5.9.3) + picomatch: 4.0.3 + rolldown: 1.0.0-beta.57 + rolldown-plugin-dts: 0.20.0(@typescript/native-preview@7.0.0-dev.20260109.1)(rolldown@1.0.0-beta.57)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.15 tree-kill: 1.2.2 unconfig-core: 7.4.2 - unrun: 0.2.19 + unrun: 0.2.24 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -6479,9 +6720,9 @@ snapshots: tslib@2.8.1: optional: true - tsx@4.20.6: + tsx@4.21.0: dependencies: - esbuild: 0.25.12 + esbuild: 0.27.2 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -6498,7 +6739,7 @@ snapshots: dependencies: content-type: 1.0.5 media-typer: 1.1.0 - mime-types: 3.0.1 + mime-types: 3.0.2 typed-array-buffer@1.0.3: dependencies: @@ -6533,13 +6774,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.49.0(eslint@9.39.1)(typescript@5.9.3): + typescript-eslint@8.52.0(eslint@9.39.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3) - eslint: 9.39.1 + '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6588,9 +6829,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unrun@0.2.19: + unrun@0.2.24: dependencies: - rolldown: 1.0.0-beta.53 + rolldown: 1.0.0-beta.59 uri-js@4.4.1: dependencies: @@ -6600,54 +6841,54 @@ snapshots: vary@1.1.2: {} - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.3)(tsx@4.20.6)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.2.2(@types/node@24.10.3)(tsx@4.20.6) + vite: 7.3.1(@types/node@24.10.4)(tsx@4.21.0) transitivePeerDependencies: - supports-color - typescript - vite@7.2.2(@types/node@24.10.3)(tsx@4.20.6): + vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0): dependencies: - esbuild: 0.25.12 + esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.53.2 + rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 fsevents: 2.3.3 - tsx: 4.20.6 + tsx: 4.21.0 - vitest@4.0.9(@types/node@24.10.3)(tsx@4.20.6): + vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0): dependencies: - '@vitest/expect': 4.0.9 - '@vitest/mocker': 4.0.9(vite@7.2.2(@types/node@24.10.3)(tsx@4.20.6)) - '@vitest/pretty-format': 4.0.9 - '@vitest/runner': 4.0.9 - '@vitest/snapshot': 4.0.9 - '@vitest/spy': 4.0.9 - '@vitest/utils': 4.0.9 - debug: 4.4.3 + '@vitest/expect': 4.0.16 + '@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0)) + '@vitest/pretty-format': 4.0.16 + '@vitest/runner': 4.0.16 + '@vitest/snapshot': 4.0.16 + '@vitest/spy': 4.0.16 + '@vitest/utils': 4.0.16 es-module-lexer: 1.7.0 - expect-type: 1.2.2 + expect-type: 1.3.0 magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.3 std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.2.2(@types/node@24.10.3)(tsx@4.20.6) + vite: 7.3.1(@types/node@24.10.4)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.10.3 + '@types/node': 24.10.4 transitivePeerDependencies: - jiti - less @@ -6657,7 +6898,6 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml @@ -6723,14 +6963,18 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.3: {} + ws@8.19.0: {} yocto-queue@0.1.0: {} - zod-to-json-schema@3.25.0(zod@3.25.76): + zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.3.5): + dependencies: + zod: 4.3.5 + zod@3.25.76: {} - zod@4.2.1: {} + zod@4.3.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1acb89261..04c20df6e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,7 +26,7 @@ catalogs: typescript: ^5.9.3 typescript-eslint: ^8.48.1 vite-tsconfig-paths: ^5.1.4 - vitest: ^4.0.8 + vitest: ^4.0.15 ws: ^8.18.0 runtimeClientOnly: cross-spawn: ^7.0.5 @@ -54,6 +54,8 @@ enableGlobalVirtualStore: false linkWorkspacePackages: deep +minimumReleaseAge: 4320 # 3 days + onlyBuiltDependencies: - better-sqlite3 - esbuild diff --git a/test/integration/package.json b/test/integration/package.json index baa099bab..ccca85f8a 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -35,7 +35,7 @@ "@modelcontextprotocol/core": "workspace:^", "@modelcontextprotocol/client": "workspace:^", "@modelcontextprotocol/server": "workspace:^", - "@modelcontextprotocol/server-express": "workspace:^", + "@modelcontextprotocol/express": "workspace:^", "zod": "catalog:runtimeShared", "vitest": "catalog:devTools", "supertest": "catalog:devTools", diff --git a/test/integration/test/server.test.ts b/test/integration/test/server.test.ts index 30a2c03c4..1cefeb62b 100644 --- a/test/integration/test/server.test.ts +++ b/test/integration/test/server.test.ts @@ -30,8 +30,8 @@ import { SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/core'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; import { InMemoryTaskStore, McpServer, Server } from '@modelcontextprotocol/server'; -import { createMcpExpressApp } from '@modelcontextprotocol/server-express'; import type { Request, Response } from 'express'; import supertest from 'supertest'; import * as z3 from 'zod/v3'; diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 90e7152aa..1b467feab 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -18,11 +18,10 @@ import { UriTemplate, UrlElicitationRequiredError } from '@modelcontextprotocol/core'; - -import { completable } from '../../../../packages/server/src/server/completable.js'; -import { McpServer, ResourceTemplate } from '../../../../packages/server/src/server/mcp.js'; -import type { ZodMatrixEntry } from '../../../../packages/server/test/server/__fixtures__/zodTestMatrix.js'; -import { zodTestMatrix } from '../../../../packages/server/test/server/__fixtures__/zodTestMatrix.js'; +import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { zodTestMatrix } from '@modelcontextprotocol/test-helpers'; +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; function createLatch() { let latch = false; diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index 666fc0509..3d742ec6d 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -8,7 +8,7 @@ "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], - "@modelcontextprotocol/server-express": ["./node_modules/@modelcontextprotocol/server-express/src/index.ts"], + "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"], "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"] } From 4c29404545456e761a419a7aeff4fdea248a9238 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 12 Jan 2026 18:24:18 +0200 Subject: [PATCH 16/23] lint, test fix --- common/vitest-config/vitest.config.js | 8 +- examples/server/package.json | 1 + examples/server/src/elicitationFormExample.ts | 3 +- examples/server/src/elicitationUrlExample.ts | 8 +- .../server/src/jsonResponseStreamableHttp.ts | 3 +- .../src/simpleStatelessStreamableHttp.ts | 3 +- examples/server/src/simpleStreamableHttp.ts | 4 +- examples/server/src/simpleTaskInteractive.ts | 2 +- examples/server/src/ssePollingExample.ts | 3 +- .../src/standaloneSseWithGetStreamableHttp.ts | 3 +- examples/server/tsconfig.json | 1 + examples/shared/package.json | 2 +- examples/shared/src/auth.ts | 2 +- packages/middleware/node/package.json | 3 +- packages/server/src/index.ts | 1 - packages/server/tsconfig.json | 2 +- pnpm-lock.yaml | 568 +++++++++--------- pnpm-workspace.yaml | 4 +- test/integration/package.json | 1 + .../stateManagementStreamableHttp.test.ts | 4 +- test/integration/test/taskLifecycle.test.ts | 4 +- .../integration/test/taskResumability.test.ts | 8 +- test/integration/tsconfig.json | 1 + 23 files changed, 325 insertions(+), 314 deletions(-) diff --git a/common/vitest-config/vitest.config.js b/common/vitest-config/vitest.config.js index 9d1a094e7..71e0d71f6 100644 --- a/common/vitest-config/vitest.config.js +++ b/common/vitest-config/vitest.config.js @@ -15,10 +15,10 @@ export default defineConfig({ deps: { moduleDirectories: ['node_modules', path.resolve(__dirname, '../../packages'), path.resolve(__dirname, '../../common')] }, - poolOptions: { - threads: { - useAtomics: true - } + }, + poolOptions: { + threads: { + useAtomics: true } }, plugins: [tsconfigPaths()] diff --git a/examples/server/package.json b/examples/server/package.json index e36dc3889..32ee5e2a9 100644 --- a/examples/server/package.json +++ b/examples/server/package.json @@ -36,6 +36,7 @@ "dependencies": { "@hono/node-server": "catalog:runtimeServerOnly", "@modelcontextprotocol/examples-shared": "workspace:^", + "@modelcontextprotocol/node": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/express": "workspace:^", "@modelcontextprotocol/hono": "workspace:^", diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index 5f82532e9..2e9f2c663 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -10,7 +10,8 @@ import { randomUUID } from 'node:crypto'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; -import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; // Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index 194788ae4..29d7294a0 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -16,13 +16,9 @@ import { setupAuthServer } from '@modelcontextprotocol/examples-shared'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult, ElicitRequestURLParams, ElicitResult } from '@modelcontextprotocol/server'; -import { - isInitializeRequest, - McpServer, - NodeStreamableHTTPServerTransport, - UrlElicitationRequiredError -} from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; diff --git a/examples/server/src/jsonResponseStreamableHttp.ts b/examples/server/src/jsonResponseStreamableHttp.ts index 6ff679f39..d04bd206e 100644 --- a/examples/server/src/jsonResponseStreamableHttp.ts +++ b/examples/server/src/jsonResponseStreamableHttp.ts @@ -1,8 +1,9 @@ import { randomUUID } from 'node:crypto'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleStatelessStreamableHttp.ts b/examples/server/src/simpleStatelessStreamableHttp.ts index 30ed060b8..00c76bb79 100644 --- a/examples/server/src/simpleStatelessStreamableHttp.ts +++ b/examples/server/src/simpleStatelessStreamableHttp.ts @@ -1,6 +1,7 @@ import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult, GetPromptResult, ReadResourceResult } from '@modelcontextprotocol/server'; -import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { McpServer } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index 5e5e19570..420513a3e 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -7,6 +7,7 @@ import { setupAuthServer } from '@modelcontextprotocol/examples-shared'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult, GetPromptResult, @@ -19,8 +20,7 @@ import { InMemoryTaskMessageQueue, InMemoryTaskStore, isInitializeRequest, - McpServer, - NodeStreamableHTTPServerTransport + McpServer } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index a9378478c..3246877a9 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -12,6 +12,7 @@ import { randomUUID } from 'node:crypto'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult, CreateMessageRequest, @@ -41,7 +42,6 @@ import { InMemoryTaskStore, isTerminal, ListToolsRequestSchema, - NodeStreamableHTTPServerTransport, RELATED_TASK_META_KEY, Server } from '@modelcontextprotocol/server'; diff --git a/examples/server/src/ssePollingExample.ts b/examples/server/src/ssePollingExample.ts index 5d0cca842..78b1fda19 100644 --- a/examples/server/src/ssePollingExample.ts +++ b/examples/server/src/ssePollingExample.ts @@ -15,8 +15,9 @@ import { randomUUID } from 'node:crypto'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { McpServer } from '@modelcontextprotocol/server'; import cors from 'cors'; import type { Request, Response } from 'express'; diff --git a/examples/server/src/standaloneSseWithGetStreamableHttp.ts b/examples/server/src/standaloneSseWithGetStreamableHttp.ts index af7bb0e04..927d26ee4 100644 --- a/examples/server/src/standaloneSseWithGetStreamableHttp.ts +++ b/examples/server/src/standaloneSseWithGetStreamableHttp.ts @@ -1,8 +1,9 @@ import { randomUUID } from 'node:crypto'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { ReadResourceResult } from '@modelcontextprotocol/server'; -import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; // Create an MCP server with implementation details diff --git a/examples/server/tsconfig.json b/examples/server/tsconfig.json index 21582de6d..e44d65ec5 100644 --- a/examples/server/tsconfig.json +++ b/examples/server/tsconfig.json @@ -7,6 +7,7 @@ "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], + "@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"], "@modelcontextprotocol/hono": ["./node_modules/@modelcontextprotocol/hono/src/index.ts"], "@modelcontextprotocol/core": [ "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" diff --git a/examples/shared/package.json b/examples/shared/package.json index 1561b6e04..3d3e7410a 100644 --- a/examples/shared/package.json +++ b/examples/shared/package.json @@ -37,7 +37,7 @@ "@modelcontextprotocol/core": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/express": "workspace:^", - "better-auth": "^1.4.7", + "better-auth": "1.4.7", "better-sqlite3": "^12.4.1", "express": "catalog:runtimeServerOnly" }, diff --git a/examples/shared/src/auth.ts b/examples/shared/src/auth.ts index 4813ce786..830d88147 100644 --- a/examples/shared/src/auth.ts +++ b/examples/shared/src/auth.ts @@ -185,7 +185,7 @@ export interface CreateDemoAuthOptions { * * @see https://www.better-auth.com/docs/plugins/mcp */ -export function createDemoAuth(options: CreateDemoAuthOptions): ReturnType { +export function createDemoAuth(options: CreateDemoAuthOptions) { const { baseURL, resource, loginPage = '/sign-in' } = options; // Use in-memory SQLite database for demo purposes diff --git a/packages/middleware/node/package.json b/packages/middleware/node/package.json index 7a93055d7..d1e1c42e7 100644 --- a/packages/middleware/node/package.json +++ b/packages/middleware/node/package.json @@ -48,7 +48,8 @@ "dependencies": { "@hono/node-server": "catalog:runtimeServerOnly", "content-type": "catalog:runtimeServerOnly", - "raw-body": "catalog:runtimeServerOnly" + "raw-body": "catalog:runtimeServerOnly", + "@modelcontextprotocol/core": "workspace:^" }, "peerDependencies": { "@modelcontextprotocol/server": "workspace:^" diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index f45be3961..7b40455d1 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,4 +1,3 @@ -export * from '../../middleware/node/src/streamableHttp.js'; export * from './server/completable.js'; export * from './server/helper/body.js'; export * from './server/mcp.js'; diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 765ab8f0f..a16bfd7d9 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@modelcontextprotocol/tsconfig", - "include": ["./", "../middleware/node/src/streamableHttp.ts", "../middleware/node/test/streamableHttp.test.ts"], + "include": ["./"], "exclude": ["node_modules", "dist"], "compilerOptions": { "paths": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1d0d1f44..33196422d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,7 +35,7 @@ catalogs: version: 8.18.1 '@typescript/native-preview': specifier: ^7.0.0-dev.20251217.1 - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: ^9.8.0 version: 9.39.2 @@ -50,7 +50,7 @@ catalogs: version: 3.6.2 supertest: specifier: ^7.0.0 - version: 7.2.2 + version: 7.1.4 tsdown: specifier: ^0.18.0 version: 0.18.4 @@ -62,7 +62,7 @@ catalogs: version: 5.9.3 typescript-eslint: specifier: ^8.48.1 - version: 8.52.0 + version: 8.51.0 vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4 @@ -71,7 +71,7 @@ catalogs: version: 4.0.16 ws: specifier: ^8.18.0 - version: 8.19.0 + version: 8.18.3 runtimeClientOnly: cross-spawn: specifier: ^7.0.5 @@ -88,7 +88,7 @@ catalogs: runtimeServerOnly: '@hono/node-server': specifier: ^1.19.7 - version: 1.19.8 + version: 1.19.7 '@remix-run/node-fetch-server': specifier: ^0.13.0 version: 0.13.0 @@ -184,7 +184,7 @@ importers: version: 8.18.1 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: catalog:devTools version: 9.39.2 @@ -199,10 +199,10 @@ importers: version: 3.6.2 supertest: specifier: catalog:devTools - version: 7.2.2 + version: 7.1.4 tsdown: specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) tsx: specifier: catalog:devTools version: 4.21.0 @@ -211,13 +211,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) ws: specifier: catalog:devTools - version: 8.19.0 + version: 8.18.3 zod: specifier: catalog:runtimeShared version: 4.3.5 @@ -242,7 +242,7 @@ importers: version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) + version: 2.32.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) eslint-plugin-n: specifier: catalog:devTools version: 17.23.1(eslint@9.39.2)(typescript@5.9.3) @@ -254,7 +254,7 @@ importers: version: 3.6.2 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) common/tsconfig: dependencies: @@ -273,7 +273,7 @@ importers: version: link:../tsconfig vite-tsconfig-paths: specifier: catalog:devTools - version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(tsx@4.21.0)) examples/client: dependencies: @@ -301,13 +301,13 @@ importers: version: link:../../common/vitest-config tsdown: specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) examples/server: dependencies: '@hono/node-server': specifier: catalog:runtimeServerOnly - version: 1.19.8(hono@4.11.3) + version: 1.19.7(hono@4.11.3) '@modelcontextprotocol/examples-shared': specifier: workspace:^ version: link:../shared @@ -317,12 +317,15 @@ importers: '@modelcontextprotocol/hono': specifier: workspace:^ version: link:../../packages/middleware/hono + '@modelcontextprotocol/node': + specifier: workspace:^ + version: link:../../packages/middleware/node '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server better-auth: specifier: ^1.4.7 - version: 1.4.10(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)) + version: 1.4.7(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)) cors: specifier: catalog:runtimeServerOnly version: 2.8.5 @@ -353,7 +356,7 @@ importers: version: 5.0.6 tsdown: specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) examples/shared: dependencies: @@ -367,8 +370,8 @@ importers: specifier: workspace:^ version: link:../../packages/server better-auth: - specifier: ^1.4.7 - version: 1.4.10(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)) + specifier: 1.4.7 + version: 1.4.7(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)) better-sqlite3: specifier: ^12.4.1 version: 12.5.0 @@ -399,7 +402,7 @@ importers: version: 5.0.6 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: catalog:devTools version: 9.39.2 @@ -420,7 +423,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) @@ -478,7 +481,7 @@ importers: version: 1.1.15 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: catalog:devTools version: 9.39.2 @@ -493,7 +496,7 @@ importers: version: 3.6.2 tsdown: specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) tsx: specifier: catalog:devTools version: 4.21.0 @@ -502,7 +505,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) @@ -560,7 +563,7 @@ importers: version: 5.1.0 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: catalog:devTools version: 9.39.2 @@ -581,7 +584,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) @@ -621,7 +624,7 @@ importers: version: 6.0.3 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: catalog:devTools version: 9.39.2 @@ -636,16 +639,16 @@ importers: version: 3.6.2 supertest: specifier: catalog:devTools - version: 7.2.2 + version: 7.1.4 tsdown: specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) @@ -673,7 +676,7 @@ importers: version: link:../../../common/vitest-config '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: catalog:devTools version: 9.39.2 @@ -688,13 +691,13 @@ importers: version: 3.6.2 tsdown: specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) typescript: specifier: catalog:devTools version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) @@ -703,7 +706,10 @@ importers: dependencies: '@hono/node-server': specifier: catalog:runtimeServerOnly - version: 1.19.8(hono@4.11.3) + version: 1.19.7(hono@4.11.3) + '@modelcontextprotocol/core': + specifier: workspace:^ + version: link:../../core content-type: specifier: catalog:runtimeServerOnly version: 1.0.5 @@ -752,7 +758,7 @@ importers: version: 6.0.3 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: catalog:devTools version: 9.39.2 @@ -767,10 +773,10 @@ importers: version: 3.6.2 supertest: specifier: catalog:devTools - version: 7.2.2 + version: 7.1.4 tsdown: specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) tsx: specifier: catalog:devTools version: 4.21.0 @@ -779,7 +785,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) @@ -822,7 +828,7 @@ importers: version: 1.1.15 '@typescript/native-preview': specifier: catalog:devTools - version: 7.0.0-dev.20260109.1 + version: 7.0.0-dev.20260105.1 eslint: specifier: catalog:devTools version: 9.39.2 @@ -837,10 +843,10 @@ importers: version: 3.6.2 supertest: specifier: catalog:devTools - version: 7.2.2 + version: 7.1.4 tsdown: specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3) + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) tsx: specifier: catalog:devTools version: 4.21.0 @@ -849,7 +855,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: catalog:devTools - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) + version: 8.51.0(eslint@9.39.2)(typescript@5.9.3) vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) @@ -895,6 +901,9 @@ importers: '@modelcontextprotocol/express': specifier: workspace:^ version: link:../../packages/middleware/express + '@modelcontextprotocol/node': + specifier: workspace:^ + version: link:../../packages/middleware/node '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server @@ -909,7 +918,7 @@ importers: version: link:../../common/vitest-config supertest: specifier: catalog:devTools - version: 7.2.2 + version: 7.1.4 vitest: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) @@ -944,20 +953,20 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@better-auth/core@1.4.10': - resolution: {integrity: sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg==} + '@better-auth/core@1.4.7': + resolution: {integrity: sha512-rNfj8aNFwPwAMYo+ahoWDsqKrV7svD3jhHSC6+A77xxKodbgV0UgH+RO21GMaZ0PPAibEl851nw5e3bsNslW/w==} peerDependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 - better-call: 1.1.7 + better-call: 1.1.5 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 - '@better-auth/telemetry@1.4.10': - resolution: {integrity: sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ==} + '@better-auth/telemetry@1.4.7': + resolution: {integrity: sha512-k07C/FWnX6m+IxLruNkCweIxuaIwVTB2X40EqwamRVhYNBAhOYZFGLHH+PtQyM+Yf1Z4+8H6MugLOXSreXNAjQ==} peerDependencies: - '@better-auth/core': 1.4.10 + '@better-auth/core': 1.4.7 '@better-auth/utils@0.3.0': resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} @@ -1232,8 +1241,8 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@hono/node-server@1.19.8': - resolution: {integrity: sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==} + '@hono/node-server@1.19.7': + resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 @@ -1286,8 +1295,8 @@ packages: resolution: {integrity: sha512-hpR5PoW0feue3LHSi1kJNhQxbySEQNWR6McuB3QCoK0zsxIdoq+id4GxRwWVOnRnjOiTecDKMD1QMfXuurDZPQ==} hasBin: true - '@modelcontextprotocol/sdk@1.25.2': - resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==} + '@modelcontextprotocol/sdk@1.25.1': + resolution: {integrity: sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -1329,8 +1338,8 @@ packages: '@oxc-project/types@0.103.0': resolution: {integrity: sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==} - '@oxc-project/types@0.107.0': - resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==} + '@oxc-project/types@0.106.0': + resolution: {integrity: sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==} '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -1347,8 +1356,8 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.0-beta.59': - resolution: {integrity: sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==} + '@rolldown/binding-android-arm64@1.0.0-beta.58': + resolution: {integrity: sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -1359,8 +1368,8 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-beta.59': - resolution: {integrity: sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==} + '@rolldown/binding-darwin-arm64@1.0.0-beta.58': + resolution: {integrity: sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -1371,8 +1380,8 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.59': - resolution: {integrity: sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==} + '@rolldown/binding-darwin-x64@1.0.0-beta.58': + resolution: {integrity: sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -1383,8 +1392,8 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.0-beta.59': - resolution: {integrity: sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.58': + resolution: {integrity: sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -1395,8 +1404,8 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': - resolution: {integrity: sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58': + resolution: {integrity: sha512-tYeXprDOrEgVHUbPXH6MPso4cM/c6RTkmJNICMQlYdki4hGMh92aj3yU6CKs+4X5gfG0yj5kVUw/L4M685SYag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -1408,8 +1417,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': - resolution: {integrity: sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58': + resolution: {integrity: sha512-N78vmZzP6zG967Ohr+MasCjmKtis0geZ1SOVmxrA0/bklTQSzH5kHEjW5Qn+i1taFno6GEre1E40v0wuWsNOQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1422,8 +1431,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': - resolution: {integrity: sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.58': + resolution: {integrity: sha512-l+p4QVtG72C7wI2SIkNQw/KQtSjuYwS3rV6AKcWrRBF62ClsFUcif5vLaZIEbPrCXu5OFRXigXFJnxYsVVZqdQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1436,8 +1445,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': - resolution: {integrity: sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.58': + resolution: {integrity: sha512-urzJX0HrXxIh0FfxwWRjfPCMeInU9qsImLQxHBgLp5ivji1EEUnOfux8KxPPnRQthJyneBrN2LeqUix9DYrNaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1450,8 +1459,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': - resolution: {integrity: sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.58': + resolution: {integrity: sha512-7ijfVK3GISnXIwq/1FZo+KyAUJjL3kWPJ7rViAL6MWeEBhEgRzJ0yEd9I8N9aut8Y8ab+EKFJyRNMWZuUBwQ0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1463,8 +1472,8 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': - resolution: {integrity: sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.58': + resolution: {integrity: sha512-/m7sKZCS+cUULbzyJTIlv8JbjNohxbpAOA6cM+lgWgqVzPee3U6jpwydrib328JFN/gF9A99IZEnuGYqEDJdww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -1474,8 +1483,8 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': - resolution: {integrity: sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.58': + resolution: {integrity: sha512-6SZk7zMgv+y3wFFQ9qE5P9NnRHcRsptL1ypmudD26PDY+PvFCvfHRkJNfclWnvacVGxjowr7JOL3a9fd1wWhUw==} engines: {node: '>=14.0.0'} cpu: [wasm32] @@ -1485,8 +1494,8 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': - resolution: {integrity: sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58': + resolution: {integrity: sha512-sFqfYPnBZ6xBhMkadB7UD0yjEDRvs7ipR3nCggblN+N4ODCXY6qhg/bKL39+W+dgQybL7ErD4EGERVbW9DAWvg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -1497,8 +1506,8 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': - resolution: {integrity: sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.58': + resolution: {integrity: sha512-AnFWJdAqB8+IDPcGrATYs67Kik/6tnndNJV2jGRmwlbeNiQQ8GhRJU8ETRlINfII0pqi9k4WWLnb00p1QCxw/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1506,8 +1515,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.57': resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} - '@rolldown/pluginutils@1.0.0-beta.59': - resolution: {integrity: sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==} + '@rolldown/pluginutils@1.0.0-beta.58': + resolution: {integrity: sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==} '@rollup/rollup-android-arm-eabi@4.55.1': resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} @@ -1734,102 +1743,102 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.52.0': - resolution: {integrity: sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==} + '@typescript-eslint/eslint-plugin@8.51.0': + resolution: {integrity: sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.52.0 + '@typescript-eslint/parser': ^8.51.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.52.0': - resolution: {integrity: sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==} + '@typescript-eslint/parser@8.51.0': + resolution: {integrity: sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.52.0': - resolution: {integrity: sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==} + '@typescript-eslint/project-service@8.51.0': + resolution: {integrity: sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.52.0': - resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==} + '@typescript-eslint/scope-manager@8.51.0': + resolution: {integrity: sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.52.0': - resolution: {integrity: sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==} + '@typescript-eslint/tsconfig-utils@8.51.0': + resolution: {integrity: sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.52.0': - resolution: {integrity: sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==} + '@typescript-eslint/type-utils@8.51.0': + resolution: {integrity: sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.52.0': - resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==} + '@typescript-eslint/types@8.51.0': + resolution: {integrity: sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.52.0': - resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==} + '@typescript-eslint/typescript-estree@8.51.0': + resolution: {integrity: sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.52.0': - resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==} + '@typescript-eslint/utils@8.51.0': + resolution: {integrity: sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.52.0': - resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} + '@typescript-eslint/visitor-keys@8.51.0': + resolution: {integrity: sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260109.1': - resolution: {integrity: sha512-rEY7JFH9JhIQ7SCjD+cpwPhIBLzNOgA7IVkfIcOpbWTmtOufx0sTZejR5B2b81x2fLCJDPZGpUv71wD1LP45iA==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260105.1': + resolution: {integrity: sha512-d+CJrdiElzHuckgsXLHlBRLbHsgzWqQuSVOZ/raF6cvBKjylnphNPx+CdtOpZrBCic0M30Q/UfTV6StMDhjIrQ==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260109.1': - resolution: {integrity: sha512-zBrxf4LYMhLGimvEZHJjtpYnpSqV4Q0rOkXEi8I5durn9NaGIBTOebBYXwF8/na6Pufdqd+vI1KQYxkm2G02pw==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260105.1': + resolution: {integrity: sha512-ntgJZDSNh7tqw2bfBuEEqhwUbuALLTLY4E/pLdCC8vaL/2QrNcTuDZX23hcLa7pQL6ML2OVvEH1MC7A1BUO/WA==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260109.1': - resolution: {integrity: sha512-WeDI+wrA1GqBFwFzj9i/zXlOuUaJdKidg6Jgry1P1TNpsHYW5YiKoNcpFirm1Fq3Dnav5cAa66z6VK5lvz7tgQ==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260105.1': + resolution: {integrity: sha512-uBj+8EPCZ8bFSosovwmgF16r4NXHgq0Wc3Ddg48KDqUl9njsVjWbg6jv3H5OXm7nnLKgRzmq9B30cHuWhlKsMg==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260109.1': - resolution: {integrity: sha512-J8kHoVttxNeMq1wdT12HPe6i/524svbdw1RsMBgb+kbqTfFFElSK7rbeZFuTfzpEA0/c6y2MY96qLP07Fq4zEw==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260105.1': + resolution: {integrity: sha512-Pv8aEc9OKG++9dPzyWwZRXt1WEHH7saAYpGrYWKnR0mMDt8yRRPX2g9ReCSFLJ/9ji3nvBg3n/wUXDXOdMQDDg==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260109.1': - resolution: {integrity: sha512-D0nuBsJTIfc1JD2HyoMKqc2Wpe0tMAP92hgwap6E3iTlKFMW9ayd7KLUjLz6EFxxw9LRw2sjECT1VvjCjAJ4GQ==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260105.1': + resolution: {integrity: sha512-vy1IpYseSpIP6X0TKnejwKH75LO06VfI7DRV9ShqgHC6Ybd7AdrTrXlna13sFF6FuD8vd3ZefesbS4n4d0Ywzw==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260109.1': - resolution: {integrity: sha512-nv365W1TiJEAAJ/NiBaKE9hSvG7U7ipuAoaSdr6HzdyMVouArq2DxxLo06mzDMOjNdc6U7vAIkLB52CcK3Y8ZQ==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260105.1': + resolution: {integrity: sha512-EFSpj3zgVsYTztgeuxhOlOpx0hr2vTVxPyccEHfiseMcAjkAsbdoSeYSLZFZyW2JXqJtq6N3YWsGGKzoyP0f+Q==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260109.1': - resolution: {integrity: sha512-da44CbC8ktr741ISLvCQlz3Gv2UqO2M+rB585xCFNjcz+0IyOKkBGr9eR++f6uy46/QKFK4w44x0cK71PVqk9g==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260105.1': + resolution: {integrity: sha512-UK0+xTxu6UZA8isOThWT9LOAl6LB1WyYFLC0ijno4SWwQCIbnJi3wjnAkBxRyM8h46DUaDYpXfHVkpYwhdExhg==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260109.1': - resolution: {integrity: sha512-27XQhOQWcGp7/nOS1NbEoC4vA2dZOmG5X+OP4e5KX2uAUc2cjE1Scn1Nnv9D7wU2ZBA+/wrqqvJqidCPFRlq+A==} + '@typescript/native-preview@7.0.0-dev.20260105.1': + resolution: {integrity: sha512-PjmhqnN/jRDLxG/5EuCe8AlW1QUEOjcDJQsxarQmMbrdW5DbSDnYUvWbYbJescXefeK0v3FUQmp7HAOCFQ7I/w==} hasBin: true '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -2070,26 +2079,26 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-auth@1.4.10: - resolution: {integrity: sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg==} + better-auth@1.4.7: + resolution: {integrity: sha512-kVmDQxzqGwP4FFMOYpS5I7oAaoFW3hwooUAAtcbb2DrOYv5EUvRUDJbTMaPoMTj7URjNDQ6vG9gcCS1Q+0aVBw==} peerDependencies: '@lynx-js/react': '*' - '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@prisma/client': ^5.22.0 '@sveltejs/kit': ^2.0.0 '@tanstack/react-start': ^1.0.0 - better-sqlite3: ^12.0.0 - drizzle-kit: '>=0.31.4' - drizzle-orm: '>=0.41.0' - mongodb: ^6.0.0 || ^7.0.0 - mysql2: ^3.0.0 + better-sqlite3: ^12.4.1 + drizzle-kit: ^0.31.4 + drizzle-orm: ^0.41.0 + mongodb: ^6.18.0 + mysql2: ^3.14.4 next: ^14.0.0 || ^15.0.0 || ^16.0.0 - pg: ^8.0.0 - prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + pg: ^8.16.3 + prisma: ^5.22.0 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 - vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vitest: ^4.0.15 vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': @@ -2129,8 +2138,8 @@ packages: vue: optional: true - better-call@1.1.7: - resolution: {integrity: sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ==} + better-call@1.1.5: + resolution: {integrity: sha512-nQJ3S87v6wApbDwbZ++FrQiSiVxWvZdjaO+2v6lZJAG2WWggkB2CziUDjPciz3eAt9TqfRursIQMZIcpkBnvlw==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -2154,8 +2163,8 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} engines: {node: '>=18'} brace-expansion@1.1.12: @@ -2797,8 +2806,8 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -3385,8 +3394,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.0-beta.59: - resolution: {integrity: sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==} + rolldown@1.0.0-beta.58: + resolution: {integrity: sha512-v1FCjMZCan7f+xGAHBi+mqiE4MlH7I+SXEHSQSJoMOGNNB2UYtvMiejsq9YuUOiZjNeUeV/a21nSFbrUR+4ZCQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3558,12 +3567,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - superagent@10.3.0: - resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + superagent@10.2.3: + resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} engines: {node: '>=14.18.0'} - supertest@7.2.2: - resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + supertest@7.1.4: + resolution: {integrity: sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==} engines: {node: '>=14.18.0'} supports-color@7.2.0: @@ -3703,8 +3712,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.52.0: - resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==} + typescript-eslint@8.51.0: + resolution: {integrity: sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3736,8 +3745,8 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unrun@0.2.24: - resolution: {integrity: sha512-xa4/O5q2jmI6EqxweJ+sOy5cyORZWcsgmi8pmABVSUyg24Fh44qJrneUHavZEMsbJbghHYWKSraFy5hDCb/m4w==} + unrun@0.2.22: + resolution: {integrity: sha512-vlQce4gTLNyCZxGylEQXGG+fSrrEFWiM/L8aghtp+t6j8xXh+lmsBtQJknG7ZSvv7P+/MRgbQtHWHBWk981uTg==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: @@ -3764,8 +3773,8 @@ packages: vite: optional: true - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3877,8 +3886,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3929,20 +3938,20 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)': + '@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@standard-schema/spec': 1.1.0 - better-call: 1.1.7(zod@4.3.5) + better-call: 1.1.5(zod@4.3.5) jose: 6.1.3 kysely: 0.28.9 nanostores: 1.1.0 zod: 4.3.5 - '@better-auth/telemetry@1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))': + '@better-auth/telemetry@1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))': dependencies: - '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -4251,7 +4260,7 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@hono/node-server@1.19.8(hono@4.11.3)': + '@hono/node-server@1.19.7(hono@4.11.3)': dependencies: hono: 4.11.3 @@ -4269,7 +4278,7 @@ snapshots: '@inquirer/external-editor@1.0.3(@types/node@24.10.4)': dependencies: chardet: 2.1.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.1 optionalDependencies: '@types/node': 24.10.4 @@ -4305,7 +4314,7 @@ snapshots: '@modelcontextprotocol/conformance@0.1.9(@cfworker/json-schema@4.1.1)(hono@4.11.3)': dependencies: - '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.3)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.25.1(@cfworker/json-schema@4.1.1)(hono@4.11.3)(zod@3.25.76) commander: 14.0.2 eventsource-parser: 3.0.6 express: 5.2.1 @@ -4316,9 +4325,9 @@ snapshots: - hono - supports-color - '@modelcontextprotocol/sdk@1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.3)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.25.1(@cfworker/json-schema@4.1.1)(hono@4.11.3)(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.8(hono@4.11.3) + '@hono/node-server': 1.19.7(hono@4.11.3) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -4374,7 +4383,7 @@ snapshots: '@oxc-project/types@0.103.0': {} - '@oxc-project/types@0.107.0': {} + '@oxc-project/types@0.106.0': {} '@paralleldrive/cuid2@2.3.1': dependencies: @@ -4389,61 +4398,61 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-android-arm64@1.0.0-beta.59': + '@rolldown/binding-android-arm64@1.0.0-beta.58': optional: true '@rolldown/binding-darwin-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.59': + '@rolldown/binding-darwin-arm64@1.0.0-beta.58': optional: true '@rolldown/binding-darwin-x64@1.0.0-beta.57': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.59': + '@rolldown/binding-darwin-x64@1.0.0-beta.58': optional: true '@rolldown/binding-freebsd-x64@1.0.0-beta.57': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.59': + '@rolldown/binding-freebsd-x64@1.0.0-beta.58': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.58': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.58': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.58': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.58': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': @@ -4451,7 +4460,7 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.58': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true @@ -4459,18 +4468,18 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.58': optional: true '@rolldown/pluginutils@1.0.0-beta.57': {} - '@rolldown/pluginutils@1.0.0-beta.59': {} + '@rolldown/pluginutils@1.0.0-beta.58': {} '@rollup/rollup-android-arm-eabi@4.55.1': optional: true @@ -4648,14 +4657,14 @@ snapshots: dependencies: '@types/node': 24.10.4 - '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/type-utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.52.0 + '@typescript-eslint/parser': 8.51.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.51.0 + '@typescript-eslint/type-utils': 8.51.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.51.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.51.0 eslint: 9.39.2 ignore: 7.0.5 natural-compare: 1.4.0 @@ -4664,41 +4673,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.52.0 + '@typescript-eslint/scope-manager': 8.51.0 + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.51.0 debug: 4.4.3 eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.52.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.51.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) - '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/tsconfig-utils': 8.51.0(typescript@5.9.3) + '@typescript-eslint/types': 8.51.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.52.0': + '@typescript-eslint/scope-manager@8.51.0': dependencies: - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/visitor-keys': 8.52.0 + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/visitor-keys': 8.51.0 - '@typescript-eslint/tsconfig-utils@8.52.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.51.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.52.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.51.0(eslint@9.39.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.51.0(eslint@9.39.2)(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.2 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -4706,14 +4715,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.52.0': {} + '@typescript-eslint/types@8.51.0': {} - '@typescript-eslint/typescript-estree@8.52.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.51.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.52.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/visitor-keys': 8.52.0 + '@typescript-eslint/project-service': 8.51.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.51.0(typescript@5.9.3) + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/visitor-keys': 8.51.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 @@ -4723,52 +4732,52 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.52.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/utils@8.51.0(eslint@9.39.2)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.51.0 + '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.52.0': + '@typescript-eslint/visitor-keys@8.51.0': dependencies: - '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/types': 8.51.0 eslint-visitor-keys: 4.2.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260109.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260105.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260109.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260105.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260109.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260105.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260109.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260105.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260109.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260105.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260109.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260105.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260109.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260105.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260109.1': + '@typescript/native-preview@7.0.0-dev.20260105.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260109.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260109.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260109.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260109.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260109.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260109.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260109.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260105.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260105.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260105.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260105.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260105.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260105.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260105.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -4838,13 +4847,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.16(vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0))': + '@vitest/mocker@4.0.16(vite@7.3.0(@types/node@24.10.4)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.0.16 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@24.10.4)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(tsx@4.21.0) '@vitest/pretty-format@4.0.16': dependencies: @@ -4986,15 +4995,15 @@ snapshots: base64-js@1.5.1: {} - better-auth@1.4.10(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)): + better-auth@1.4.7(better-sqlite3@12.5.0)(vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0)): dependencies: - '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) + '@better-auth/core': 1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.7(@better-auth/core@1.4.7(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.5(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 - better-call: 1.1.7(zod@4.3.5) + better-call: 1.1.5(zod@4.3.5) defu: 6.1.4 jose: 6.1.3 kysely: 0.28.9 @@ -5004,7 +5013,7 @@ snapshots: better-sqlite3: 12.5.0 vitest: 4.0.16(@types/node@24.10.4)(tsx@4.21.0) - better-call@1.1.7(zod@4.3.5): + better-call@1.1.5(zod@4.3.5): dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -5034,13 +5043,13 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@2.2.2: + body-parser@2.2.1: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.1 on-finished: 2.4.1 qs: 6.14.1 raw-body: 3.0.2 @@ -5392,15 +5401,15 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.51.0(eslint@9.39.2)(typescript@5.9.3) eslint: 9.39.2 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.2) @@ -5414,7 +5423,7 @@ snapshots: eslint: 9.39.2 eslint-compat-utils: 0.5.1(eslint@9.39.2) - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5425,7 +5434,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5437,7 +5446,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.51.0(eslint@9.39.2)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5553,7 +5562,7 @@ snapshots: express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.2 + body-parser: 2.2.1 content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 @@ -5801,7 +5810,7 @@ snapshots: human-id@4.1.3: {} - iconv-lite@0.7.2: + iconv-lite@0.7.1: dependencies: safer-buffer: 2.1.2 @@ -6256,7 +6265,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.1 unpipe: 1.0.0 rc@1.2.8: @@ -6315,7 +6324,7 @@ snapshots: reusify@1.1.0: {} - rolldown-plugin-dts@0.20.0(@typescript/native-preview@7.0.0-dev.20260109.1)(rolldown@1.0.0-beta.57)(typescript@5.9.3): + rolldown-plugin-dts@0.20.0(@typescript/native-preview@7.0.0-dev.20260105.1)(rolldown@1.0.0-beta.57)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -6327,7 +6336,7 @@ snapshots: obug: 2.1.1 rolldown: 1.0.0-beta.57 optionalDependencies: - '@typescript/native-preview': 7.0.0-dev.20260109.1 + '@typescript/native-preview': 7.0.0-dev.20260105.1 typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver @@ -6351,24 +6360,24 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.57 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.57 - rolldown@1.0.0-beta.59: + rolldown@1.0.0-beta.58: dependencies: - '@oxc-project/types': 0.107.0 - '@rolldown/pluginutils': 1.0.0-beta.59 + '@oxc-project/types': 0.106.0 + '@rolldown/pluginutils': 1.0.0-beta.58 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.59 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.59 - '@rolldown/binding-darwin-x64': 1.0.0-beta.59 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.59 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.59 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.59 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.59 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.59 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.59 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.59 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.59 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.59 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.59 + '@rolldown/binding-android-arm64': 1.0.0-beta.58 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.58 + '@rolldown/binding-darwin-x64': 1.0.0-beta.58 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.58 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.58 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.58 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.58 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.58 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.58 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.58 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.58 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.58 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.58 rollup@4.55.1: dependencies: @@ -6602,7 +6611,7 @@ snapshots: strip-json-comments@3.1.1: {} - superagent@10.3.0: + superagent@10.2.3: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 @@ -6616,11 +6625,10 @@ snapshots: transitivePeerDependencies: - supports-color - supertest@7.2.2: + supertest@7.1.4: dependencies: - cookie-signature: 1.2.2 methods: 1.1.2 - superagent: 10.3.0 + superagent: 10.2.3 transitivePeerDependencies: - supports-color @@ -6690,7 +6698,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsdown@0.18.4(@typescript/native-preview@7.0.0-dev.20260109.1)(typescript@5.9.3): + tsdown@0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3): dependencies: ansis: 4.2.0 cac: 6.7.14 @@ -6701,13 +6709,13 @@ snapshots: obug: 2.1.1 picomatch: 4.0.3 rolldown: 1.0.0-beta.57 - rolldown-plugin-dts: 0.20.0(@typescript/native-preview@7.0.0-dev.20260109.1)(rolldown@1.0.0-beta.57)(typescript@5.9.3) + rolldown-plugin-dts: 0.20.0(@typescript/native-preview@7.0.0-dev.20260105.1)(rolldown@1.0.0-beta.57)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.15 tree-kill: 1.2.2 unconfig-core: 7.4.2 - unrun: 0.2.24 + unrun: 0.2.22 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -6774,12 +6782,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.52.0(eslint@9.39.2)(typescript@5.9.3): + typescript-eslint@8.51.0(eslint@9.39.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.51.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.51.0(eslint@9.39.2)(typescript@5.9.3) eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: @@ -6829,9 +6837,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unrun@0.2.24: + unrun@0.2.22: dependencies: - rolldown: 1.0.0-beta.59 + rolldown: 1.0.0-beta.58 uri-js@4.4.1: dependencies: @@ -6841,18 +6849,18 @@ snapshots: vary@1.1.2: {} - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(tsx@4.21.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.3.1(@types/node@24.10.4)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(tsx@4.21.0) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0): + vite@7.3.0(@types/node@24.10.4)(tsx@4.21.0): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -6868,7 +6876,7 @@ snapshots: vitest@4.0.16(@types/node@24.10.4)(tsx@4.21.0): dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@24.10.4)(tsx@4.21.0)) + '@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@24.10.4)(tsx@4.21.0)) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -6885,7 +6893,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@24.10.4)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.4 @@ -6963,7 +6971,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.19.0: {} + ws@8.18.3: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 04c20df6e..159c41f5b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -54,7 +54,9 @@ enableGlobalVirtualStore: false linkWorkspacePackages: deep -minimumReleaseAge: 4320 # 3 days +minimumReleaseAge: 10080 # 7 days +minimumReleaseAgeExclude: + - '@modelcontextprotocol/conformance' onlyBuiltDependencies: - better-sqlite3 diff --git a/test/integration/package.json b/test/integration/package.json index ccca85f8a..8b6022d26 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -36,6 +36,7 @@ "@modelcontextprotocol/client": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/express": "workspace:^", + "@modelcontextprotocol/node": "workspace:^", "zod": "catalog:runtimeShared", "vitest": "catalog:devTools", "supertest": "catalog:devTools", diff --git a/test/integration/test/stateManagementStreamableHttp.test.ts b/test/integration/test/stateManagementStreamableHttp.test.ts index 72180b688..3aa944ed0 100644 --- a/test/integration/test/stateManagementStreamableHttp.test.ts +++ b/test/integration/test/stateManagementStreamableHttp.test.ts @@ -3,14 +3,14 @@ import type { Server } from 'node:http'; import { createServer } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import { CallToolResultSchema, LATEST_PROTOCOL_VERSION, ListPromptsResultSchema, ListResourcesResultSchema, ListToolsResultSchema, - McpServer, - NodeStreamableHTTPServerTransport + McpServer } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; diff --git a/test/integration/test/taskLifecycle.test.ts b/test/integration/test/taskLifecycle.test.ts index 324da6aa2..3d56a71f1 100644 --- a/test/integration/test/taskLifecycle.test.ts +++ b/test/integration/test/taskLifecycle.test.ts @@ -3,6 +3,7 @@ import type { Server } from 'node:http'; import { createServer } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { TaskRequestOptions } from '@modelcontextprotocol/server'; import { CallToolResultSchema, @@ -14,7 +15,6 @@ import { InMemoryTaskStore, McpError, McpServer, - NodeStreamableHTTPServerTransport, RELATED_TASK_META_KEY, TaskSchema } from '@modelcontextprotocol/server'; @@ -1041,7 +1041,7 @@ describe('Task Lifecycle Integration Tests', () => { method: 'tasks/cancel', params: { taskId } }, - z.object({ _meta: z.record(z.unknown()).optional() }) + z.object({ _meta: z.record(z.string(), z.unknown()).optional() }) ); // Verify task is cancelled diff --git a/test/integration/test/taskResumability.test.ts b/test/integration/test/taskResumability.test.ts index db60e2d4e..744d17b31 100644 --- a/test/integration/test/taskResumability.test.ts +++ b/test/integration/test/taskResumability.test.ts @@ -3,13 +3,9 @@ import type { Server } from 'node:http'; import { createServer } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server'; -import { - CallToolResultSchema, - LoggingMessageNotificationSchema, - McpServer, - NodeStreamableHTTPServerTransport -} from '@modelcontextprotocol/server'; +import { CallToolResultSchema, LoggingMessageNotificationSchema, McpServer } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index 3d742ec6d..5c22ec1be 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -9,6 +9,7 @@ "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], + "@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"], "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"] } From ce58234cf767ff69c33c36b83e4a46bde5cc8005 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 13 Jan 2026 11:01:25 +0200 Subject: [PATCH 17/23] auth examples - demo flag, configs --- examples/server/src/elicitationUrlExample.ts | 2 +- examples/server/src/simpleStreamableHttp.ts | 2 +- examples/shared/src/auth.ts | 36 +++++++++------- examples/shared/src/authServer.ts | 42 +++++++++++-------- .../test/demoInMemoryOAuthProvider.test.ts | 6 ++- 5 files changed, 50 insertions(+), 38 deletions(-) diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index 29d7294a0..f76277f6f 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -235,7 +235,7 @@ let authMiddleware = null; const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`); const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); -setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true }); +setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true, demoMode: true }); // Add protected resource metadata route to the MCP server // This allows clients to discover the auth server diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index 420513a3e..509d9ec73 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -527,7 +527,7 @@ if (useOAuth) { const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`); const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); - setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth }); + setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth, demoMode: true }); // Add protected resource metadata route to the MCP server // This allows clients to discover the auth server diff --git a/examples/shared/src/auth.ts b/examples/shared/src/auth.ts index 830d88147..783c524d2 100644 --- a/examples/shared/src/auth.ts +++ b/examples/shared/src/auth.ts @@ -9,6 +9,7 @@ import { randomBytes } from 'node:crypto'; +import type { BetterAuthOptions } from 'better-auth'; import { betterAuth } from 'better-auth'; import { mcp } from 'better-auth/plugins'; import Database from 'better-sqlite3'; @@ -173,6 +174,7 @@ export interface CreateDemoAuthOptions { baseURL: string; resource?: string; loginPage?: string; + demoMode: boolean; } /** @@ -186,7 +188,7 @@ export interface CreateDemoAuthOptions { * @see https://www.better-auth.com/docs/plugins/mcp */ export function createDemoAuth(options: CreateDemoAuthOptions) { - const { baseURL, resource, loginPage = '/sign-in' } = options; + const { baseURL, resource, loginPage = '/sign-in', demoMode } = options; // Use in-memory SQLite database for demo purposes // Note: All data is lost on restart - demo only! @@ -214,7 +216,7 @@ export function createDemoAuth(options: CreateDemoAuthOptions) { baseURL, // eslint-disable-next-line @typescript-eslint/no-explicit-any database: db as any, // Type cast to avoid exposing better-sqlite3 in exported types - trustedOrigins: ['*'], + trustedOrigins: [baseURL.toString()], // Basic email+password for demo emailAndPassword: { enabled: true, @@ -222,20 +224,22 @@ export function createDemoAuth(options: CreateDemoAuthOptions) { }, plugins: [mcpPlugin], // Enable verbose logging for demo/debugging - logger: { - disabled: false, - level: 'debug', - log: (level, message, ...args) => { - const timestamp = new Date().toISOString(); - const prefix = `[Auth ${level.toUpperCase()}]`; - if (args.length > 0) { - console.log(`${timestamp} ${prefix} ${message}`, ...args); - } else { - console.log(`${timestamp} ${prefix} ${message}`); - } - } - } - }); + logger: demoMode + ? { + disabled: false, + level: 'debug', + log: (level, message, ...args) => { + const timestamp = new Date().toISOString(); + const prefix = `[Auth ${level.toUpperCase()}]`; + if (args.length > 0) { + console.log(`${timestamp} ${prefix} ${message}`, ...args); + } else { + console.log(`${timestamp} ${prefix} ${message}`); + } + } + } + : undefined + } satisfies BetterAuthOptions); } /** diff --git a/examples/shared/src/authServer.ts b/examples/shared/src/authServer.ts index 1a99146bc..963cba9b8 100644 --- a/examples/shared/src/authServer.ts +++ b/examples/shared/src/authServer.ts @@ -21,6 +21,10 @@ export interface SetupAuthServerOptions { authServerUrl: URL; mcpServerUrl: URL; strictResource?: boolean; + /** + * Examples should be used for **demo** only and not for production purposes, however this mode disables some logging and other features. + */ + demoMode: boolean; } // Store auth instance globally so it can be used for token verification @@ -75,13 +79,14 @@ async function ensureDemoUserExists(auth: DemoAuth): Promise { * @param options - Server configuration */ export function setupAuthServer(options: SetupAuthServerOptions): void { - const { authServerUrl, mcpServerUrl } = options; + const { authServerUrl, mcpServerUrl, demoMode } = options; // Create better-auth instance with MCP plugin const auth = createDemoAuth({ baseURL: authServerUrl.toString().replace(/\/$/, ''), resource: mcpServerUrl.toString(), - loginPage: '/sign-in' + loginPage: '/sign-in', + demoMode: demoMode }); // Store globally for token verification @@ -111,23 +116,25 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { console.log(`${timestamp} [Auth Request] Content-Type: ${req.headers['content-type']}`); } - // Log response when it finishes - const originalSend = res.send.bind(res); - res.send = function (body) { - console.log(`${timestamp} [Auth Response] ${res.statusCode} ${req.url}`); - if (res.statusCode >= 400 && body) { - try { - const parsed = typeof body === 'string' ? JSON.parse(body) : body; - console.log(`${timestamp} [Auth Response] Error:`, parsed); - } catch { - // Not JSON, log as-is if short - if (typeof body === 'string' && body.length < 200) { - console.log(`${timestamp} [Auth Response] Body: ${body}`); + if (demoMode) { + // Log response when it finishes + const originalSend = res.send.bind(res); + res.send = function (body) { + console.log(`${timestamp} [Auth Response] ${res.statusCode} ${req.url}`); + if (res.statusCode >= 400 && body) { + try { + const parsed = typeof body === 'string' ? JSON.parse(body) : body; + console.log(`${timestamp} [Auth Response] Error:`, parsed); + } catch { + // Not JSON, log as-is if short + if (typeof body === 'string' && body.length < 200) { + console.log(`${timestamp} [Auth Response] Body: ${body}`); + } } } - } - return originalSend(body); - }; + return originalSend(body); + }; + } next(); }); @@ -137,7 +144,6 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { // OAuth metadata endpoints using better-auth's built-in handlers authApp.get('/.well-known/oauth-authorization-server', toNodeHandler(oAuthDiscoveryMetadata(auth))); - authApp.get('/.well-known/oauth-protected-resource', toNodeHandler(oAuthProtectedResourceMetadata(auth))); // Body parsers for non-better-auth routes (like /sign-in) authApp.use(express.json()); diff --git a/examples/shared/test/demoInMemoryOAuthProvider.test.ts b/examples/shared/test/demoInMemoryOAuthProvider.test.ts index 1c798047f..bd3131dba 100644 --- a/examples/shared/test/demoInMemoryOAuthProvider.test.ts +++ b/examples/shared/test/demoInMemoryOAuthProvider.test.ts @@ -16,7 +16,8 @@ describe('createDemoAuth', () => { const validOptions: CreateDemoAuthOptions = { baseURL: 'http://localhost:3001', resource: 'http://localhost:3000/mcp', - loginPage: '/sign-in' + loginPage: '/sign-in', + demoMode: true }; it('creates a better-auth instance with MCP plugin', () => { @@ -27,7 +28,8 @@ describe('createDemoAuth', () => { it('uses default loginPage when not specified', () => { const options: CreateDemoAuthOptions = { - baseURL: 'http://localhost:3001' + baseURL: 'http://localhost:3001', + demoMode: true }; const auth = createDemoAuth(options); expect(auth).toBeDefined(); From 635c37ba7fe4633c7c8475dddad806af2cf4b566 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 13 Jan 2026 13:09:15 +0200 Subject: [PATCH 18/23] package clean up, peer deps on middleware, comment fix --- packages/core/src/shared/protocol.ts | 2 +- packages/middleware/express/package.json | 10 ++++----- packages/middleware/express/tsdown.config.ts | 6 ++--- packages/middleware/hono/package.json | 5 ++++- packages/middleware/hono/tsdown.config.ts | 6 ++--- packages/middleware/node/package.json | 14 ++---------- packages/middleware/node/tsdown.config.ts | 5 +---- pnpm-lock.yaml | 23 +++++--------------- pnpm-workspace.yaml | 1 - 9 files changed, 23 insertions(+), 49 deletions(-) diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index c9242e96d..90c6116e0 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -299,7 +299,7 @@ export type RequestHandlerExtra void; diff --git a/packages/middleware/express/package.json b/packages/middleware/express/package.json index 5185ceffa..473ebbaea 100644 --- a/packages/middleware/express/package.json +++ b/packages/middleware/express/package.json @@ -44,24 +44,24 @@ "test:watch": "vitest" }, "dependencies": { - "@modelcontextprotocol/server": "workspace:^", - "express": "catalog:runtimeServerOnly", - "@remix-run/node-fetch-server": "catalog:runtimeServerOnly" + "express": "catalog:runtimeServerOnly" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "workspace:^" }, "devDependencies": { + "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", "@eslint/js": "catalog:devTools", "@types/express": "catalog:devTools", "@types/express-serve-static-core": "catalog:devTools", - "@types/supertest": "catalog:devTools", "@typescript/native-preview": "catalog:devTools", "eslint": "catalog:devTools", "eslint-config-prettier": "catalog:devTools", "eslint-plugin-n": "catalog:devTools", "prettier": "catalog:devTools", - "supertest": "catalog:devTools", "tsdown": "catalog:devTools", "typescript": "catalog:devTools", "typescript-eslint": "catalog:devTools", diff --git a/packages/middleware/express/tsdown.config.ts b/packages/middleware/express/tsdown.config.ts index c72e7a2c4..c8283cb97 100644 --- a/packages/middleware/express/tsdown.config.ts +++ b/packages/middleware/express/tsdown.config.ts @@ -14,10 +14,8 @@ export default defineConfig({ compilerOptions: { baseUrl: '.', paths: { - '@modelcontextprotocol/server': ['../server/src/index.ts'], - '@modelcontextprotocol/core': ['../core/src/index.ts'] + '@modelcontextprotocol/server': ['../server/src/index.ts'] } } - }, - noExternal: ['@modelcontextprotocol/server', '@modelcontextprotocol/core'] + } }); diff --git a/packages/middleware/hono/package.json b/packages/middleware/hono/package.json index 255b82b49..6a2f505bb 100644 --- a/packages/middleware/hono/package.json +++ b/packages/middleware/hono/package.json @@ -44,10 +44,13 @@ "test:watch": "vitest" }, "dependencies": { - "@modelcontextprotocol/server": "workspace:^", "hono": "catalog:runtimeServerOnly" }, + "peerDependencies": { + "@modelcontextprotocol/server": "workspace:^" + }, "devDependencies": { + "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", diff --git a/packages/middleware/hono/tsdown.config.ts b/packages/middleware/hono/tsdown.config.ts index c72e7a2c4..c8283cb97 100644 --- a/packages/middleware/hono/tsdown.config.ts +++ b/packages/middleware/hono/tsdown.config.ts @@ -14,10 +14,8 @@ export default defineConfig({ compilerOptions: { baseUrl: '.', paths: { - '@modelcontextprotocol/server': ['../server/src/index.ts'], - '@modelcontextprotocol/core': ['../core/src/index.ts'] + '@modelcontextprotocol/server': ['../server/src/index.ts'] } } - }, - noExternal: ['@modelcontextprotocol/server', '@modelcontextprotocol/core'] + } }); diff --git a/packages/middleware/node/package.json b/packages/middleware/node/package.json index d1e1c42e7..766346613 100644 --- a/packages/middleware/node/package.json +++ b/packages/middleware/node/package.json @@ -46,34 +46,24 @@ "client": "tsx scripts/cli.ts client" }, "dependencies": { - "@hono/node-server": "catalog:runtimeServerOnly", - "content-type": "catalog:runtimeServerOnly", - "raw-body": "catalog:runtimeServerOnly", - "@modelcontextprotocol/core": "workspace:^" + "@hono/node-server": "catalog:runtimeServerOnly" }, "peerDependencies": { "@modelcontextprotocol/server": "workspace:^" }, "devDependencies": { "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/core": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", "@modelcontextprotocol/test-helpers": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", "@eslint/js": "catalog:devTools", - "@types/content-type": "catalog:devTools", - "@types/cors": "catalog:devTools", - "@types/cross-spawn": "catalog:devTools", - "@types/eventsource": "catalog:devTools", - "@types/express": "catalog:devTools", - "@types/express-serve-static-core": "catalog:devTools", - "@types/supertest": "catalog:devTools", "@typescript/native-preview": "catalog:devTools", "eslint": "catalog:devTools", "eslint-config-prettier": "catalog:devTools", "eslint-plugin-n": "catalog:devTools", "prettier": "catalog:devTools", - "supertest": "catalog:devTools", "tsdown": "catalog:devTools", "tsx": "catalog:devTools", "typescript": "catalog:devTools", diff --git a/packages/middleware/node/tsdown.config.ts b/packages/middleware/node/tsdown.config.ts index c3d38817a..8eb1b630c 100644 --- a/packages/middleware/node/tsdown.config.ts +++ b/packages/middleware/node/tsdown.config.ts @@ -27,8 +27,5 @@ export default defineConfig({ '@modelcontextprotocol/core': ['../core/src/index.ts'] } } - }, - // 5. Vendoring Strategy - Bundle the code for this specific package into the output, - // but treat all other dependencies as external (require/import). - noExternal: ['@modelcontextprotocol/core'] + } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33196422d..c40889928 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,9 +89,6 @@ catalogs: '@hono/node-server': specifier: ^1.19.7 version: 1.19.7 - '@remix-run/node-fetch-server': - specifier: ^0.13.0 - version: 0.13.0 content-type: specifier: ^1.0.5 version: 1.0.5 @@ -591,12 +588,6 @@ importers: packages/middleware/express: dependencies: - '@modelcontextprotocol/server': - specifier: workspace:^ - version: link:../../server - '@remix-run/node-fetch-server': - specifier: catalog:runtimeServerOnly - version: 0.13.0 express: specifier: catalog:runtimeServerOnly version: 5.2.1 @@ -607,6 +598,9 @@ importers: '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../../common/eslint-config + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../../server '@modelcontextprotocol/tsconfig': specifier: workspace:^ version: link:../../../common/tsconfig @@ -655,9 +649,6 @@ importers: packages/middleware/hono: dependencies: - '@modelcontextprotocol/server': - specifier: workspace:^ - version: link:../../server hono: specifier: catalog:runtimeServerOnly version: 4.11.3 @@ -668,6 +659,9 @@ importers: '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../../common/eslint-config + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../../server '@modelcontextprotocol/tsconfig': specifier: workspace:^ version: link:../../../common/tsconfig @@ -1347,9 +1341,6 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@remix-run/node-fetch-server@0.13.0': - resolution: {integrity: sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA==} - '@rolldown/binding-android-arm64@1.0.0-beta.57': resolution: {integrity: sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4393,8 +4384,6 @@ snapshots: dependencies: quansync: 1.0.0 - '@remix-run/node-fetch-server@0.13.0': {} - '@rolldown/binding-android-arm64@1.0.0-beta.57': optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 159c41f5b..61f34ddb3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -35,7 +35,6 @@ catalogs: jose: ^6.1.1 runtimeServerOnly: '@hono/node-server': ^1.19.7 - '@remix-run/node-fetch-server': ^0.13.0 content-type: ^1.0.5 cors: ^2.8.5 express: ^5.2.1 From c154cbb5cbeab5b988080f0288b0736765b01fdf Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 13 Jan 2026 13:11:34 +0200 Subject: [PATCH 19/23] peer deps update --- packages/middleware/express/package.json | 7 ++-- packages/middleware/hono/package.json | 7 ++-- pnpm-lock.yaml | 48 ++---------------------- 3 files changed, 9 insertions(+), 53 deletions(-) diff --git a/packages/middleware/express/package.json b/packages/middleware/express/package.json index 473ebbaea..408cf446a 100644 --- a/packages/middleware/express/package.json +++ b/packages/middleware/express/package.json @@ -43,11 +43,10 @@ "test": "vitest run", "test:watch": "vitest" }, - "dependencies": { - "express": "catalog:runtimeServerOnly" - }, + "dependencies": {}, "peerDependencies": { - "@modelcontextprotocol/server": "workspace:^" + "@modelcontextprotocol/server": "workspace:^", + "express": "catalog:runtimeServerOnly" }, "devDependencies": { "@modelcontextprotocol/server": "workspace:^", diff --git a/packages/middleware/hono/package.json b/packages/middleware/hono/package.json index 6a2f505bb..3377c5fb4 100644 --- a/packages/middleware/hono/package.json +++ b/packages/middleware/hono/package.json @@ -43,11 +43,10 @@ "test": "vitest run", "test:watch": "vitest" }, - "dependencies": { - "hono": "catalog:runtimeServerOnly" - }, + "dependencies": {}, "peerDependencies": { - "@modelcontextprotocol/server": "workspace:^" + "@modelcontextprotocol/server": "workspace:^", + "hono": "catalog:runtimeServerOnly" }, "devDependencies": { "@modelcontextprotocol/server": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c40889928..a70090eb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,9 +89,6 @@ catalogs: '@hono/node-server': specifier: ^1.19.7 version: 1.19.7 - content-type: - specifier: ^1.0.5 - version: 1.0.5 cors: specifier: ^2.8.5 version: 2.8.5 @@ -101,9 +98,6 @@ catalogs: hono: specifier: ^4.11.1 version: 4.11.3 - raw-body: - specifier: ^3.0.0 - version: 3.0.2 runtimeShared: '@cfworker/json-schema': specifier: ^4.1.1 @@ -613,9 +607,6 @@ importers: '@types/express-serve-static-core': specifier: catalog:devTools version: 5.1.0 - '@types/supertest': - specifier: catalog:devTools - version: 6.0.3 '@typescript/native-preview': specifier: catalog:devTools version: 7.0.0-dev.20260105.1 @@ -631,9 +622,6 @@ importers: prettier: specifier: catalog:devTools version: 3.6.2 - supertest: - specifier: catalog:devTools - version: 7.1.4 tsdown: specifier: catalog:devTools version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) @@ -701,19 +689,13 @@ importers: '@hono/node-server': specifier: catalog:runtimeServerOnly version: 1.19.7(hono@4.11.3) - '@modelcontextprotocol/core': - specifier: workspace:^ - version: link:../../core - content-type: - specifier: catalog:runtimeServerOnly - version: 1.0.5 - raw-body: - specifier: catalog:runtimeServerOnly - version: 3.0.2 devDependencies: '@eslint/js': specifier: catalog:devTools version: 9.39.2 + '@modelcontextprotocol/core': + specifier: workspace:^ + version: link:../../core '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../../common/eslint-config @@ -729,27 +711,6 @@ importers: '@modelcontextprotocol/vitest-config': specifier: workspace:^ version: link:../../../common/vitest-config - '@types/content-type': - specifier: catalog:devTools - version: 1.1.9 - '@types/cors': - specifier: catalog:devTools - version: 2.8.19 - '@types/cross-spawn': - specifier: catalog:devTools - version: 6.0.6 - '@types/eventsource': - specifier: catalog:devTools - version: 1.1.15 - '@types/express': - specifier: catalog:devTools - version: 5.0.6 - '@types/express-serve-static-core': - specifier: catalog:devTools - version: 5.1.0 - '@types/supertest': - specifier: catalog:devTools - version: 6.0.3 '@typescript/native-preview': specifier: catalog:devTools version: 7.0.0-dev.20260105.1 @@ -765,9 +726,6 @@ importers: prettier: specifier: catalog:devTools version: 3.6.2 - supertest: - specifier: catalog:devTools - version: 7.1.4 tsdown: specifier: catalog:devTools version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260105.1)(typescript@5.9.3) From 96fd273d7d1c8b5a23197a5f569e4620a3655b37 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 13 Jan 2026 14:14:38 +0200 Subject: [PATCH 20/23] update READMEs --- CLAUDE.md | 7 ++- README.md | 28 +++++++++++ packages/middleware/README.md | 24 ++++++++++ packages/middleware/express/README.md | 67 +++++++++------------------ packages/middleware/hono/README.md | 46 ++++++------------ packages/middleware/node/README.md | 56 ++++++++++++++++++++++ 6 files changed, 152 insertions(+), 76 deletions(-) create mode 100644 packages/middleware/README.md create mode 100644 packages/middleware/node/README.md diff --git a/CLAUDE.md b/CLAUDE.md index 3caca17b6..0f6eaeece 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,11 +64,15 @@ Transports (`packages/core/src/shared/transport.ts`) provide the communication l ### Client-Side Features - **Auth**: OAuth client support in `packages/client/src/client/auth.ts` and `packages/client/src/client/auth-extensions.ts` -- **Middleware**: Request middleware in `packages/client/src/client/middleware.ts` +- **Client middleware**: Request middleware in `packages/client/src/client/middleware.ts` (unrelated to the framework adapter packages below) - **Sampling**: Clients can handle `sampling/createMessage` requests from servers (LLM completions) - **Elicitation**: Clients can handle `elicitation/create` requests for user input (form or URL mode) - **Roots**: Clients can expose filesystem roots to servers via `roots/list` +### Middleware packages (framework/runtime adapters) + +The repo also ships “middleware” packages under `packages/middleware/` (e.g. `@modelcontextprotocol/express`, `@modelcontextprotocol/hono`, `@modelcontextprotocol/node`). These are thin integration layers for specific frameworks/runtimes and should not add new MCP functionality. + ### Experimental Features Located in `packages/*/src/experimental/`: @@ -224,6 +228,7 @@ mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, extra) => { ```typescript // Server +// (Node.js IncomingMessage/ServerResponse wrapper; exported by @modelcontextprotocol/node) const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); await server.connect(transport); diff --git a/README.md b/README.md index 4d5270287..45bea6518 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This repository contains the TypeScript SDK implementation of the MCP specificat - MCP **server** libraries (tools/resources/prompts, Streamable HTTP, stdio, auth helpers) - MCP **client** libraries (transports, high-level helpers, OAuth helpers) +- Optional **middleware packages** for specific runtimes/frameworks (Express, Hono, Node.js HTTP) - Runnable **examples** (under [`examples/`](examples/)) ## Packages @@ -40,6 +41,16 @@ This monorepo publishes split packages: Both packages have a **required peer dependency** on `zod` for schema validation. The SDK internally imports from `zod/v4`, but remains compatible with projects using Zod v3.25+. +### Middleware packages (optional) + +The SDK also publishes small “middleware” packages under [`packages/middleware/`](packages/middleware/) that help you **wire MCP into a specific runtime or web framework**. + +They are intentionally thin adapters: they should not introduce new MCP functionality or business logic. See [`packages/middleware/README.md`](packages/middleware/README.md) for details. + +- **`@modelcontextprotocol/node`**: Node.js Streamable HTTP transport wrapper for `IncomingMessage` / `ServerResponse` +- **`@modelcontextprotocol/express`**: Express helpers (app defaults + Host header validation) +- **`@modelcontextprotocol/hono`**: Hono helpers (app defaults + JSON body parsing hook + Host header validation) + ## Installation ### Server @@ -54,6 +65,23 @@ npm install @modelcontextprotocol/server zod npm install @modelcontextprotocol/client zod ``` +### Optional middleware packages + +The SDK also publishes optional “middleware” packages that help you **wire MCP into a specific runtime or web framework** (for example Express, Hono, or Node.js `http`). + +These packages are intentionally thin adapters and should not introduce additional MCP features or business logic. See [`packages/middleware/README.md`](packages/middleware/README.md) for details. + +```bash +# Node.js HTTP (IncomingMessage/ServerResponse) Streamable HTTP transport: +npm install @modelcontextprotocol/node + +# Express integration: +npm install @modelcontextprotocol/express express + +# Hono integration: +npm install @modelcontextprotocol/hono hono +``` + ## Quick Start (runnable examples) The runnable examples live under `examples/` and are kept in sync with the docs. diff --git a/packages/middleware/README.md b/packages/middleware/README.md new file mode 100644 index 000000000..de892a2ef --- /dev/null +++ b/packages/middleware/README.md @@ -0,0 +1,24 @@ +# Middleware packages + +The packages in `packages/middleware/*` are **thin integration layers** that help you expose an MCP server in a specific runtime, platform, or web framework. + +They intentionally **do not** add new MCP features or “business logic”. MCP functionality (tools, resources, prompts, transports, auth primitives, etc.) lives in `@modelcontextprotocol/server` (and other core packages). Middleware packages should primarily: + +- adapt request/response types to the SDK (e.g. Node.js `IncomingMessage`/`ServerResponse`) +- provide small framework helpers (e.g. wiring, body parsing hooks) +- supply safe defaults for common deployment pitfalls (e.g. localhost DNS rebinding protection) + +## Packages + +- `@modelcontextprotocol/express` — Express helpers (app defaults + Host header validation for DNS rebinding protection). +- `@modelcontextprotocol/hono` — Hono helpers (app defaults + JSON body parsing hook + Host header validation). +- `@modelcontextprotocol/node` — Node.js Streamable HTTP transport wrapper for `IncomingMessage`/`ServerResponse`. + +## Typical usage + +Most servers use: + +- `@modelcontextprotocol/server` for the MCP server implementation +- one middleware package for framework/runtime integration (this folder) +- (optionally) additional platform/framework dependencies (Express, Hono, etc.) + diff --git a/packages/middleware/express/README.md b/packages/middleware/express/README.md index 6191149ae..4bd12d2f1 100644 --- a/packages/middleware/express/README.md +++ b/packages/middleware/express/README.md @@ -2,26 +2,31 @@ Express adapters for the MCP TypeScript server SDK. -This package is the Express-specific companion to [`@modelcontextprotocol/server`](../server/), which is framework-agnostic and uses Web Standard `Request`/`Response` interfaces. +This package is a thin Express integration layer for [`@modelcontextprotocol/server`](../../server/). + +It does **not** implement MCP itself. Instead, it helps you: + +- create an Express app with sensible defaults for MCP servers +- add DNS rebinding protection via Host header validation (recommended for localhost servers) ## Install ```bash -npm install @modelcontextprotocol/server @modelcontextprotocol/express zod +npm install @modelcontextprotocol/server @modelcontextprotocol/express express + +# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse): +npm install @modelcontextprotocol/node ``` ## Exports - `createMcpExpressApp(options?)` -- `hostHeaderValidation(allowedHosts)` +- `hostHeaderValidation(allowedHostnames)` - `localhostHostValidation()` -- `mcpAuthRouter(options)` -- `mcpAuthMetadataRouter(options)` -- `requireBearerAuth(options)` ## Usage -### Create an Express app with localhost DNS rebinding protection +### Create an Express app (localhost DNS rebinding protection by default) ```ts import { createMcpExpressApp } from '@modelcontextprotocol/express'; @@ -32,52 +37,26 @@ const app = createMcpExpressApp(); // default host is 127.0.0.1; protection enab ### Streamable HTTP endpoint (Express) ```ts -import { McpServer, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; const app = createMcpExpressApp(); +const server = new McpServer({ name: 'my-server', version: '1.0.0' }); app.post('/mcp', async (req, res) => { - const transport = new NodeStreamableHTTPServerTransport(); - await transport.handleRequest(req, res, req.body); + // Stateless example: create a transport per request. + // For stateful mode (sessions), keep a transport instance around and reuse it. + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); }); ``` -### OAuth routes (Express) - -`@modelcontextprotocol/server` provides Web-standard auth handlers; this package wraps them as Express routers. +### Host header validation (DNS rebinding protection) ```ts -import { mcpAuthRouter } from '@modelcontextprotocol/express'; -import type { OAuthServerProvider } from '@modelcontextprotocol/server'; -import express from 'express'; - -const provider: OAuthServerProvider = /* ... */; -const app = express(); -app.use(express.json()); - -// MUST be mounted at the app root -app.use( - mcpAuthRouter({ - provider, - issuerUrl: new URL('https://auth.example.com'), - // Optional rate limiting (implemented via express-rate-limit) - rateLimit: { windowMs: 60_000, max: 60 } - }) -); -``` - -### Bearer auth middleware (Express) +import { hostHeaderValidation } from '@modelcontextprotocol/express'; -`requireBearerAuth` validates the `Authorization: Bearer ...` header and sets `req.auth` on success. - -```ts -import { requireBearerAuth } from '@modelcontextprotocol/express'; -import type { OAuthTokenVerifier } from '@modelcontextprotocol/server'; - -const verifier: OAuthTokenVerifier = /* ... */; - -app.post('/protected', requireBearerAuth({ verifier }), (req, res) => { - res.json({ clientId: req.auth?.clientId }); -}); +app.use(hostHeaderValidation(['localhost', '127.0.0.1', '[::1]'])); ``` diff --git a/packages/middleware/hono/README.md b/packages/middleware/hono/README.md index 5e6ea5831..a7339bf68 100644 --- a/packages/middleware/hono/README.md +++ b/packages/middleware/hono/README.md @@ -2,20 +2,24 @@ Hono adapters for the MCP TypeScript server SDK. -This package is the Hono-specific companion to [`@modelcontextprotocol/server`](../server/), which is framework-agnostic and uses Web Standard `Request`/`Response` interfaces. +This package is a thin Hono integration layer for [`@modelcontextprotocol/server`](../../server/). + +It does **not** implement MCP itself. Instead, it helps you: + +- create a Hono app with sensible defaults for MCP servers +- parse JSON request bodies and expose them as `c.get('parsedBody')` for Streamable HTTP transports +- add DNS rebinding protection via Host header validation (recommended for localhost servers) ## Install ```bash -npm install @modelcontextprotocol/server @modelcontextprotocol/hono hono zod +npm install @modelcontextprotocol/server @modelcontextprotocol/hono hono ``` ## Exports -- `mcpStreamableHttpHandler(transport)` -- `registerMcpAuthRoutes(app, options)` -- `registerMcpAuthMetadataRoutes(app, options)` -- `hostHeaderValidation(allowedHosts)` +- `createMcpHonoApp(options?)` +- `hostHeaderValidation(allowedHostnames)` - `localhostHostValidation()` ## Usage @@ -23,42 +27,22 @@ npm install @modelcontextprotocol/server @modelcontextprotocol/hono hono zod ### Streamable HTTP endpoint (Hono) ```ts -import { Hono } from 'hono'; import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import { mcpStreamableHttpHandler } from '@modelcontextprotocol/hono'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; const server = new McpServer({ name: 'my-server', version: '1.0.0' }); -const transport = new WebStandardStreamableHTTPServerTransport(); +const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await server.connect(transport); -const app = new Hono(); -app.all('/mcp', mcpStreamableHttpHandler(transport)); -``` - -### OAuth routes (Hono) - -`@modelcontextprotocol/server` provides Web-standard auth handlers; this package mounts them onto a Hono app. - -```ts -import { Hono } from 'hono'; -import type { OAuthServerProvider } from '@modelcontextprotocol/server'; -import { registerMcpAuthRoutes } from '@modelcontextprotocol/hono'; - -const provider: OAuthServerProvider = /* ... */; - -const app = new Hono(); -registerMcpAuthRoutes(app, { - provider, - issuerUrl: new URL('https://auth.example.com') -}); +const app = createMcpHonoApp(); +app.all('/mcp', c => transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') })); ``` ### Host header validation (DNS rebinding protection) ```ts -import { Hono } from 'hono'; import { localhostHostValidation } from '@modelcontextprotocol/hono'; -const app = new Hono(); +const app = createMcpHonoApp(); app.use('*', localhostHostValidation()); ``` diff --git a/packages/middleware/node/README.md b/packages/middleware/node/README.md new file mode 100644 index 000000000..33a21353f --- /dev/null +++ b/packages/middleware/node/README.md @@ -0,0 +1,56 @@ +# `@modelcontextprotocol/node` + +Node.js adapters for the MCP TypeScript server SDK. + +This package is a thin Node.js integration layer for [`@modelcontextprotocol/server`](../../server/). It provides a Streamable HTTP transport that works with Node’s `IncomingMessage` / `ServerResponse`. + +For web‑standard runtimes (Cloudflare Workers, Deno, Bun, etc.), use `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly. + +## Install + +```bash +npm install @modelcontextprotocol/server @modelcontextprotocol/node +``` + +## Exports + +- `NodeStreamableHTTPServerTransport` +- `StreamableHTTPServerTransportOptions` (type alias for `WebStandardStreamableHTTPServerTransportOptions`) + +## Usage + +### Express + Streamable HTTP + +```ts +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; + +const server = new McpServer({ name: 'my-server', version: '1.0.0' }); +const app = createMcpExpressApp(); + +app.post('/mcp', async (req, res) => { + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); + + // If you use Express JSON parsing, pass the pre-parsed body to avoid re-reading the stream. + await transport.handleRequest(req, res, req.body); +}); +``` + +### Node.js `http` server + +```ts +import { createServer } from 'node:http'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; + +const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + +createServer(async (req, res) => { + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); + await transport.handleRequest(req, res); +}).listen(3000); +``` + From c174fe43723eae4d26fdb520083bd04976396c0f Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 13 Jan 2026 14:17:38 +0200 Subject: [PATCH 21/23] lint fix --- packages/middleware/express/README.md | 10 +++++----- packages/middleware/node/README.md | 15 +++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/middleware/express/README.md b/packages/middleware/express/README.md index 4bd12d2f1..386141d14 100644 --- a/packages/middleware/express/README.md +++ b/packages/middleware/express/README.md @@ -45,11 +45,11 @@ const app = createMcpExpressApp(); const server = new McpServer({ name: 'my-server', version: '1.0.0' }); app.post('/mcp', async (req, res) => { - // Stateless example: create a transport per request. - // For stateful mode (sessions), keep a transport instance around and reuse it. - const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - await server.connect(transport); - await transport.handleRequest(req, res, req.body); + // Stateless example: create a transport per request. + // For stateful mode (sessions), keep a transport instance around and reuse it. + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); }); ``` diff --git a/packages/middleware/node/README.md b/packages/middleware/node/README.md index 33a21353f..678e1d452 100644 --- a/packages/middleware/node/README.md +++ b/packages/middleware/node/README.md @@ -30,11 +30,11 @@ const server = new McpServer({ name: 'my-server', version: '1.0.0' }); const app = createMcpExpressApp(); app.post('/mcp', async (req, res) => { - const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - await server.connect(transport); + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); - // If you use Express JSON parsing, pass the pre-parsed body to avoid re-reading the stream. - await transport.handleRequest(req, res, req.body); + // If you use Express JSON parsing, pass the pre-parsed body to avoid re-reading the stream. + await transport.handleRequest(req, res, req.body); }); ``` @@ -48,9 +48,8 @@ import { McpServer } from '@modelcontextprotocol/server'; const server = new McpServer({ name: 'my-server', version: '1.0.0' }); createServer(async (req, res) => { - const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - await server.connect(transport); - await transport.handleRequest(req, res); + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); + await transport.handleRequest(req, res); }).listen(3000); ``` - From 70c348d4dcc77794da4b217c14bfc37b9a2513fd Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 13 Jan 2026 19:29:52 +0200 Subject: [PATCH 22/23] merge commit --- package.json | 1 + pnpm-lock.yaml | 3 +++ src/conformance/everything-server.ts | 8 ++++---- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d5dd62d78..1181d9e4b 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@modelcontextprotocol/client": "workspace:^", "@modelcontextprotocol/conformance": "0.1.9", "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/node": "workspace:^", "@types/content-type": "catalog:devTools", "@types/cors": "catalog:devTools", "@types/cross-spawn": "catalog:devTools", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 974b8429e..66600384f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,6 +146,9 @@ importers: '@modelcontextprotocol/conformance': specifier: 0.1.9 version: 0.1.9(@cfworker/json-schema@4.1.1)(hono@4.11.3) + '@modelcontextprotocol/node': + specifier: workspace:^ + version: link:packages/middleware/node '@modelcontextprotocol/server': specifier: workspace:^ version: link:packages/server diff --git a/src/conformance/everything-server.ts b/src/conformance/everything-server.ts index a4a40b403..92500cb37 100644 --- a/src/conformance/everything-server.ts +++ b/src/conformance/everything-server.ts @@ -24,10 +24,10 @@ import { SetLevelRequestSchema, McpServer, ResourceTemplate, - StreamableHTTPServerTransport, SubscribeRequestSchema, UnsubscribeRequestSchema } from '@modelcontextprotocol/server'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { Request, Response } from 'express'; import cors from 'cors'; import express from 'express'; @@ -38,7 +38,7 @@ const resourceSubscriptions = new Set(); const watchedResourceContent = 'Watched resource content'; // Session management -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; +const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; const servers: { [sessionId: string]: McpServer } = {}; // In-memory event store for SEP-1699 resumability @@ -920,7 +920,7 @@ app.post('/mcp', async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; try { - let transport: StreamableHTTPServerTransport; + let transport: NodeStreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { // Reuse existing transport for established sessions @@ -929,7 +929,7 @@ app.post('/mcp', async (req: Request, res: Response) => { // Create new transport for initialization requests const mcpServer = createMcpServer(); - transport = new StreamableHTTPServerTransport({ + transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore: createEventStore(), retryInterval: 5000, // 5 second retry interval for SEP-1699 From ed9c27bc0ede62906e811b1c851be88c020ef2e5 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 13 Jan 2026 23:15:57 +0200 Subject: [PATCH 23/23] fix merge --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index bb612447d..4690b14d1 100644 --- a/.gitignore +++ b/.gitignore @@ -133,10 +133,7 @@ dist/ # IDE .idea/ -<<<<<<< HEAD .cursor/ -======= # Conformance test results results/ ->>>>>>> 0073ab212cd5632068e93629344a0274ee67749a