From 1d5d5d61e6e555778461c5ddc6b6cbeb27031b6f Mon Sep 17 00:00:00 2001 From: Andy Fleming Date: Sat, 14 Feb 2026 09:59:22 -0800 Subject: [PATCH 1/3] Add fastify middleware --- packages/middleware/fastify/README.md | 62 ++++ packages/middleware/fastify/eslint.config.mjs | 12 + packages/middleware/fastify/package.json | 67 ++++ .../fastify/src/fastify.examples.ts | 41 +++ packages/middleware/fastify/src/fastify.ts | 83 +++++ packages/middleware/fastify/src/index.ts | 2 + .../hostHeaderValidation.examples.ts | 30 ++ .../src/middleware/hostHeaderValidation.ts | 49 +++ .../middleware/fastify/test/fastify.test.ts | 237 +++++++++++++ packages/middleware/fastify/tsconfig.json | 15 + packages/middleware/fastify/tsdown.config.ts | 21 ++ packages/middleware/fastify/typedoc.json | 10 + packages/middleware/fastify/vitest.config.js | 3 + pnpm-lock.yaml | 313 ++++++++++++++++++ pnpm-workspace.yaml | 1 + 15 files changed, 946 insertions(+) create mode 100644 packages/middleware/fastify/README.md create mode 100644 packages/middleware/fastify/eslint.config.mjs create mode 100644 packages/middleware/fastify/package.json create mode 100644 packages/middleware/fastify/src/fastify.examples.ts create mode 100644 packages/middleware/fastify/src/fastify.ts create mode 100644 packages/middleware/fastify/src/index.ts create mode 100644 packages/middleware/fastify/src/middleware/hostHeaderValidation.examples.ts create mode 100644 packages/middleware/fastify/src/middleware/hostHeaderValidation.ts create mode 100644 packages/middleware/fastify/test/fastify.test.ts create mode 100644 packages/middleware/fastify/tsconfig.json create mode 100644 packages/middleware/fastify/tsdown.config.ts create mode 100644 packages/middleware/fastify/typedoc.json create mode 100644 packages/middleware/fastify/vitest.config.js diff --git a/packages/middleware/fastify/README.md b/packages/middleware/fastify/README.md new file mode 100644 index 000000000..5cb0426a4 --- /dev/null +++ b/packages/middleware/fastify/README.md @@ -0,0 +1,62 @@ +# `@modelcontextprotocol/fastify` + +Fastify adapters for the MCP TypeScript server SDK. + +This package is a thin Fastify integration layer for [`@modelcontextprotocol/server`](../../server/). + +It does **not** implement MCP itself. Instead, it helps you: + +- create a Fastify 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/fastify fastify + +# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse): +npm install @modelcontextprotocol/node +``` + +## Exports + +- `createMcpFastifyApp(options?)` +- `hostHeaderValidation(allowedHostnames)` +- `localhostHostValidation()` + +## Usage + +### Create a Fastify app (localhost DNS rebinding protection by default) + +```ts +import { createMcpFastifyApp } from '@modelcontextprotocol/fastify'; + +const app = createMcpFastifyApp(); // default host is 127.0.0.1; protection enabled +``` + +### Streamable HTTP endpoint (Fastify) + +```ts +import { createMcpFastifyApp } from '@modelcontextprotocol/fastify'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; + +const app = createMcpFastifyApp(); +const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + +app.post('/mcp', async (request, reply) => { + // 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(request.raw, reply.raw, request.body); +}); +``` + +### Host header validation (DNS rebinding protection) + +```ts +import { hostHeaderValidation } from '@modelcontextprotocol/fastify'; + +app.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]'])); +``` diff --git a/packages/middleware/fastify/eslint.config.mjs b/packages/middleware/fastify/eslint.config.mjs new file mode 100644 index 000000000..03d533134 --- /dev/null +++ b/packages/middleware/fastify/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/middleware/fastify/package.json b/packages/middleware/fastify/package.json new file mode 100644 index 000000000..719426b55 --- /dev/null +++ b/packages/middleware/fastify/package.json @@ -0,0 +1,67 @@ +{ + "name": "@modelcontextprotocol/fastify", + "private": false, + "version": "2.0.0-alpha.0", + "description": "Fastify adapters for the Model Context Protocol TypeScript server SDK - Fastify 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", + "fastify", + "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" + }, + "dependencies": {}, + "peerDependencies": { + "@modelcontextprotocol/server": "workspace:^", + "fastify": "catalog:runtimeServerOnly" + }, + "devDependencies": { + "@modelcontextprotocol/server": "workspace:^", + "@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/middleware/fastify/src/fastify.examples.ts b/packages/middleware/fastify/src/fastify.examples.ts new file mode 100644 index 000000000..36353ced7 --- /dev/null +++ b/packages/middleware/fastify/src/fastify.examples.ts @@ -0,0 +1,41 @@ +/** + * Type-checked examples for `fastify.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { createMcpFastifyApp } from './fastify.js'; + +/** + * Example: Basic usage with default DNS rebinding protection. + */ +function createMcpFastifyApp_default() { + //#region createMcpFastifyApp_default + const app = createMcpFastifyApp(); + //#endregion createMcpFastifyApp_default + return app; +} + +/** + * Example: Custom host binding with and without DNS rebinding protection. + */ +function createMcpFastifyApp_customHost() { + //#region createMcpFastifyApp_customHost + const appOpen = createMcpFastifyApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection + const appLocal = createMcpFastifyApp({ host: 'localhost' }); // DNS rebinding protection enabled + //#endregion createMcpFastifyApp_customHost + return { appOpen, appLocal }; +} + +/** + * Example: Custom allowed hosts for non-localhost binding. + */ +function createMcpFastifyApp_allowedHosts() { + //#region createMcpFastifyApp_allowedHosts + const app = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); + //#endregion createMcpFastifyApp_allowedHosts + return app; +} diff --git a/packages/middleware/fastify/src/fastify.ts b/packages/middleware/fastify/src/fastify.ts new file mode 100644 index 000000000..ad84d1eb5 --- /dev/null +++ b/packages/middleware/fastify/src/fastify.ts @@ -0,0 +1,83 @@ +import type { FastifyInstance } from 'fastify'; +import Fastify from 'fastify'; + +import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; + +/** + * Options for creating an MCP Fastify application. + */ +export interface CreateMcpFastifyAppOptions { + /** + * 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 Fastify 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 is automatically applied via an onRequest hook to protect against + * DNS rebinding attacks on localhost servers. + * + * Fastify parses JSON request bodies by default, so no additional middleware is required + * for MCP Streamable HTTP endpoints. + * + * @param options - Configuration options + * @returns A configured Fastify application + * + * @example Basic usage - defaults to 127.0.0.1 with DNS rebinding protection + * ```ts source="./fastify.examples.ts#createMcpFastifyApp_default" + * const app = createMcpFastifyApp(); + * ``` + * + * @example Custom host - DNS rebinding protection only applied for localhost hosts + * ```ts source="./fastify.examples.ts#createMcpFastifyApp_customHost" + * const appOpen = createMcpFastifyApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection + * const appLocal = createMcpFastifyApp({ host: 'localhost' }); // DNS rebinding protection enabled + * ``` + * + * @example Custom allowed hosts for non-localhost binding + * ```ts source="./fastify.examples.ts#createMcpFastifyApp_allowedHosts" + * const app = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); + * ``` + */ +export function createMcpFastifyApp(options: CreateMcpFastifyAppOptions = {}): FastifyInstance { + const { host = '127.0.0.1', allowedHosts } = options; + + const app = Fastify(); + + // Fastify parses JSON by default - no middleware needed + + // If allowedHosts is explicitly provided, use that for validation + if (allowedHosts) { + app.addHook('onRequest', hostHeaderValidation(allowedHosts)); + } else { + // Apply DNS rebinding protection automatically for localhost hosts + const localhostHosts = ['127.0.0.1', 'localhost', '::1']; + if (localhostHosts.includes(host)) { + app.addHook('onRequest', 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/middleware/fastify/src/index.ts b/packages/middleware/fastify/src/index.ts new file mode 100644 index 000000000..5c852617b --- /dev/null +++ b/packages/middleware/fastify/src/index.ts @@ -0,0 +1,2 @@ +export * from './fastify.js'; +export * from './middleware/hostHeaderValidation.js'; diff --git a/packages/middleware/fastify/src/middleware/hostHeaderValidation.examples.ts b/packages/middleware/fastify/src/middleware/hostHeaderValidation.examples.ts new file mode 100644 index 000000000..cbf664584 --- /dev/null +++ b/packages/middleware/fastify/src/middleware/hostHeaderValidation.examples.ts @@ -0,0 +1,30 @@ +/** + * Type-checked examples for `hostHeaderValidation.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { FastifyInstance } from 'fastify'; + +import { hostHeaderValidation, localhostHostValidation } from './hostHeaderValidation.js'; + +/** + * Example: Using hostHeaderValidation hook with custom allowed hosts. + */ +function hostHeaderValidation_basicUsage(app: FastifyInstance) { + //#region hostHeaderValidation_basicUsage + app.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]'])); + //#endregion hostHeaderValidation_basicUsage +} + +/** + * Example: Using localhostHostValidation convenience hook. + */ +function localhostHostValidation_basicUsage(app: FastifyInstance) { + //#region localhostHostValidation_basicUsage + app.addHook('onRequest', localhostHostValidation()); + //#endregion localhostHostValidation_basicUsage +} diff --git a/packages/middleware/fastify/src/middleware/hostHeaderValidation.ts b/packages/middleware/fastify/src/middleware/hostHeaderValidation.ts new file mode 100644 index 000000000..41b3b2478 --- /dev/null +++ b/packages/middleware/fastify/src/middleware/hostHeaderValidation.ts @@ -0,0 +1,49 @@ +import { localhostAllowedHostnames, validateHostHeader } from '@modelcontextprotocol/server'; +import type { FastifyReply, FastifyRequest } from 'fastify'; + +/** + * Fastify onRequest hook 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 Fastify onRequest hook handler + * + * @example + * ```ts source="./hostHeaderValidation.examples.ts#hostHeaderValidation_basicUsage" + * app.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]'])); + * ``` + */ +export function hostHeaderValidation(allowedHostnames: string[]) { + return async (request: FastifyRequest, reply: FastifyReply): Promise => { + const result = validateHostHeader(request.headers.host, allowedHostnames); + if (!result.ok) { + await reply.code(403).send({ + jsonrpc: '2.0', + error: { + code: -32_000, + message: result.message + }, + id: null + }); + } + }; +} + +/** + * Convenience hook for localhost DNS rebinding protection. + * Allows only `localhost`, `127.0.0.1`, and `[::1]` (IPv6 localhost) hostnames. + * + * @example + * ```ts source="./hostHeaderValidation.examples.ts#localhostHostValidation_basicUsage" + * app.addHook('onRequest', localhostHostValidation()); + * ``` + */ +export function localhostHostValidation() { + return hostHeaderValidation(localhostAllowedHostnames()); +} diff --git a/packages/middleware/fastify/test/fastify.test.ts b/packages/middleware/fastify/test/fastify.test.ts new file mode 100644 index 000000000..3176f311f --- /dev/null +++ b/packages/middleware/fastify/test/fastify.test.ts @@ -0,0 +1,237 @@ +import Fastify from 'fastify'; +import { vi } from 'vitest'; + +import { createMcpFastifyApp } from '../src/fastify.js'; +import { hostHeaderValidation, localhostHostValidation } from '../src/middleware/hostHeaderValidation.js'; + +describe('@modelcontextprotocol/fastify', () => { + describe('hostHeaderValidation', () => { + test('should block invalid Host header', async () => { + const app = Fastify(); + app.addHook('onRequest', hostHeaderValidation(['localhost'])); + app.get('/health', async () => ({ ok: true })); + + const res = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'evil.com:3000' } + }); + + expect(res.statusCode).toBe(403); + expect(res.json()).toEqual( + expect.objectContaining({ + jsonrpc: '2.0', + error: expect.objectContaining({ + code: -32_000 + }), + id: null + }) + ); + }); + + test('should allow valid Host header', async () => { + const app = Fastify(); + app.addHook('onRequest', hostHeaderValidation(['localhost'])); + app.get('/health', async () => 'ok'); + + const res = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'localhost:3000' } + }); + + expect(res.statusCode).toBe(200); + expect(res.body).toBe('ok'); + }); + + test('should handle multiple allowed hostnames', async () => { + const app = Fastify(); + app.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', 'myapp.local'])); + app.get('/health', async () => 'ok'); + + const res1 = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: '127.0.0.1:8080' } + }); + const res2 = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'myapp.local' } + }); + + expect(res1.statusCode).toBe(200); + expect(res2.statusCode).toBe(200); + }); + }); + + describe('localhostHostValidation', () => { + test('should allow localhost', async () => { + const app = Fastify(); + app.addHook('onRequest', localhostHostValidation()); + app.get('/health', async () => 'ok'); + + const res = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'localhost:3000' } + }); + expect(res.statusCode).toBe(200); + }); + + test('should allow 127.0.0.1', async () => { + const app = Fastify(); + app.addHook('onRequest', localhostHostValidation()); + app.get('/health', async () => 'ok'); + + const res = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: '127.0.0.1:3000' } + }); + expect(res.statusCode).toBe(200); + }); + + test('should allow [::1] (IPv6 localhost)', async () => { + const app = Fastify(); + app.addHook('onRequest', localhostHostValidation()); + app.get('/health', async () => 'ok'); + + const res = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: '[::1]:3000' } + }); + expect(res.statusCode).toBe(200); + }); + + test('should block non-localhost hosts', async () => { + const app = Fastify(); + app.addHook('onRequest', localhostHostValidation()); + app.get('/health', async () => 'ok'); + + const res = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'evil.com:3000' } + }); + expect(res.statusCode).toBe(403); + }); + }); + + describe('createMcpFastifyApp', () => { + test('should enable localhost DNS rebinding protection by default', async () => { + const app = createMcpFastifyApp(); + app.get('/health', async () => 'ok'); + + const bad = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'evil.com:3000' } + }); + expect(bad.statusCode).toBe(403); + + const good = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'localhost:3000' } + }); + expect(good.statusCode).toBe(200); + }); + + test('should apply DNS rebinding protection for localhost host', () => { + const app = createMcpFastifyApp({ host: 'localhost' }); + expect(app).toBeDefined(); + expect(typeof app.addHook).toBe('function'); + expect(typeof app.get).toBe('function'); + expect(typeof app.post).toBe('function'); + }); + + test('should apply DNS rebinding protection for ::1 host', () => { + const app = createMcpFastifyApp({ host: '::1' }); + expect(app).toBeDefined(); + }); + + test('should use allowedHosts when provided', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const app = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); + warn.mockRestore(); + + app.get('/health', async () => 'ok'); + + const bad = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'evil.com:3000' } + }); + expect(bad.statusCode).toBe(403); + + const good = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'myapp.local:3000' } + }); + expect(good.statusCode).toBe(200); + }); + + test('should warn when binding to 0.0.0.0 without allowedHosts', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + createMcpFastifyApp({ 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(() => {}); + + createMcpFastifyApp({ 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(() => {}); + + createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); + + expect(warn).not.toHaveBeenCalled(); + + warn.mockRestore(); + }); + + test('should not apply host validation for 0.0.0.0 without allowedHosts', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const app = createMcpFastifyApp({ host: '0.0.0.0' }); + warn.mockRestore(); + + app.get('/health', async () => 'ok'); + + const res = await app.inject({ + method: 'GET', + url: '/health', + headers: { host: 'evil.com:3000' } + }); + expect(res.statusCode).toBe(200); + }); + + test('should not apply host validation for non-localhost hosts without allowedHosts', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const app = createMcpFastifyApp({ host: '192.168.1.1' }); + + expect(warn).not.toHaveBeenCalled(); + expect(app).toBeDefined(); + + warn.mockRestore(); + }); + }); +}); diff --git a/packages/middleware/fastify/tsconfig.json b/packages/middleware/fastify/tsconfig.json new file mode 100644 index 000000000..c92435851 --- /dev/null +++ b/packages/middleware/fastify/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "*": ["./*"], + "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], + "@modelcontextprotocol/core": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + ] + } + } +} diff --git a/packages/middleware/fastify/tsdown.config.ts b/packages/middleware/fastify/tsdown.config.ts new file mode 100644 index 000000000..c8283cb97 --- /dev/null +++ b/packages/middleware/fastify/tsdown.config.ts @@ -0,0 +1,21 @@ +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'] + } + } + } +}); diff --git a/packages/middleware/fastify/typedoc.json b/packages/middleware/fastify/typedoc.json new file mode 100644 index 000000000..dd7007942 --- /dev/null +++ b/packages/middleware/fastify/typedoc.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src"], + "entryPointStrategy": "expand", + "exclude": ["**/*.test.ts"], + "navigation": { + "includeGroups": true, + "includeCategories": true + } +} diff --git a/packages/middleware/fastify/vitest.config.js b/packages/middleware/fastify/vitest.config.js new file mode 100644 index 000000000..496fca320 --- /dev/null +++ b/packages/middleware/fastify/vitest.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2099eab0f..d77b93a6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ catalogs: express: specifier: ^5.2.1 version: 5.2.1 + fastify: + specifier: ^5.2.0 + version: 5.7.4 hono: specifier: ^4.11.4 version: 4.11.4 @@ -656,6 +659,55 @@ importers: specifier: catalog:devTools version: 4.0.16(@types/node@24.10.4)(tsx@4.21.0)(yaml@2.8.2) + packages/middleware/fastify: + dependencies: + fastify: + specifier: catalog:runtimeServerOnly + version: 5.7.4 + devDependencies: + '@eslint/js': + specifier: catalog:devTools + version: 9.39.2 + '@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 + '@modelcontextprotocol/vitest-config': + specifier: workspace:^ + version: link:../../../common/vitest-config + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20260105.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.20260105.1)(typescript@5.9.3) + typescript: + specifier: catalog:devTools + version: 5.9.3 + typescript-eslint: + specifier: catalog:devTools + 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)(yaml@2.8.2) + packages/middleware/hono: dependencies: hono: @@ -1459,6 +1511,24 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@gerrit0/mini-shiki@3.20.0': resolution: {integrity: sha512-Wa57i+bMpK6PGJZ1f2myxo3iO+K/kZikcyvH8NIqNNZhQUbDav7V9LQmWOXhf946mz5c1NZ19WMsGYiDKTryzQ==} @@ -1721,6 +1791,9 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -2384,6 +2457,9 @@ packages: '@vitest/utils@4.0.16': resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -2480,10 +2556,17 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2783,6 +2866,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -3084,6 +3171,9 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3094,15 +3184,24 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastify@5.7.4: + resolution: {integrity: sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3130,6 +3229,10 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-my-way@9.4.0: + resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} + engines: {node: '>=20'} + find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -3346,6 +3449,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3489,6 +3596,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3523,6 +3633,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} @@ -3698,6 +3811,10 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3794,6 +3911,16 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.0: + resolution: {integrity: sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA==} + hasBin: true + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -3829,6 +3956,12 @@ packages: engines: {node: '>=14'} hasBin: true + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3857,6 +3990,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -3877,6 +4013,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3913,10 +4053,17 @@ packages: engines: {node: '>= 0.4'} hasBin: true + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown-plugin-dts@0.20.0: resolution: {integrity: sha512-cLAY1kN2ilTYMfZcFlGWbXnu6Nb+8uwUBsi+Mjbh4uIx7IN8uMOmJ7RxrrRgPsO4H7eSz3E+JwGoL1gyugiyUA==} engines: {node: '>=20.19.0'} @@ -3976,9 +4123,19 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4059,6 +4216,9 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4066,6 +4226,10 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -4157,6 +4321,10 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4176,6 +4344,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -4985,6 +5157,29 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@gerrit0/mini-shiki@3.20.0': dependencies: '@shikijs/engine-oniguruma': 3.20.0 @@ -5223,6 +5418,8 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@pinojs/redact@0.4.0': {} + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -5751,6 +5948,8 @@ snapshots: '@vitest/pretty-format': 4.0.16 tinyrainbow: 3.0.3 + abstract-logging@2.0.1: {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -5861,10 +6060,17 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 + avvio@9.1.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -6106,6 +6312,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -6564,6 +6772,8 @@ snapshots: extendable-error@0.1.7: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -6576,12 +6786,43 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-safe-stringify@2.1.1: {} fast-uri@3.1.0: {} + fastify@5.7.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.4.0 + light-my-request: 6.6.0 + pino: 10.3.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.3 + toad-cache: 3.7.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -6611,6 +6852,12 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way@9.4.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + find-up-simple@1.0.1: {} find-up@4.1.0: @@ -6822,6 +7069,8 @@ snapshots: ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -6965,6 +7214,10 @@ snapshots: json-buffer@3.0.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -6994,6 +7247,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 @@ -7148,6 +7407,8 @@ snapshots: obug@2.1.1: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -7229,6 +7490,26 @@ snapshots: pify@4.0.1: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 4.0.0 + pkce-challenge@5.0.1: {} pluralize@8.0.0: {} @@ -7262,6 +7543,10 @@ snapshots: prettier@3.6.2: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -7286,6 +7571,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + range-parser@1.2.1: {} raw-body@3.0.2: @@ -7315,6 +7602,8 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + real-require@0.2.0: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -7355,8 +7644,12 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + ret@0.5.0: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + 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 @@ -7480,8 +7773,16 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -7616,6 +7917,10 @@ snapshots: slash@3.0.0: {} + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} spawndamnit@3.0.1: @@ -7623,6 +7928,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + split2@4.2.0: {} + sprintf-js@1.0.3: {} stable-hash-x@0.2.0: {} @@ -7725,6 +8032,10 @@ snapshots: term-size@2.2.1: {} + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinyexec@1.0.2: {} @@ -7740,6 +8051,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} tr46@0.0.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0eaa89471..68a020486 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -40,6 +40,7 @@ catalogs: content-type: ^1.0.5 cors: ^2.8.5 express: ^5.2.1 + fastify: ^5.2.0 hono: ^4.11.4 raw-body: ^3.0.0 runtimeShared: From 11631a0727fda93132acabc133ade5e23e7b9a2a Mon Sep 17 00:00:00 2001 From: Andy Fleming Date: Sat, 14 Feb 2026 10:04:02 -0800 Subject: [PATCH 2/3] Add states and method notes to README --- packages/middleware/fastify/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/middleware/fastify/README.md b/packages/middleware/fastify/README.md index 5cb0426a4..fdfa50132 100644 --- a/packages/middleware/fastify/README.md +++ b/packages/middleware/fastify/README.md @@ -42,17 +42,25 @@ import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import { McpServer } from '@modelcontextprotocol/server'; const app = createMcpFastifyApp(); -const server = new McpServer({ name: 'my-server', version: '1.0.0' }); +const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' }); app.post('/mcp', async (request, reply) => { // 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 mcpServer.connect(transport); + + // Clean up when the client closes the connection (e.g. during SSE streaming). + reply.raw.on('close', () => { + transport.close(); + }); + await transport.handleRequest(request.raw, reply.raw, request.body); }); ``` +If you create a new `McpServer` per request in stateless mode, also call `mcpServer.close()` in the `close` handler. To reject non-POST requests with 405 Method Not Allowed, add routes for GET and DELETE that send a JSON-RPC error response. + ### Host header validation (DNS rebinding protection) ```ts From f72b2e979fbd44fbb8b578d7002fce58aba9d174 Mon Sep 17 00:00:00 2001 From: Andy Fleming Date: Sat, 14 Feb 2026 11:59:48 -0800 Subject: [PATCH 3/3] Use the fastify logger instead of console.warn --- packages/middleware/fastify/src/fastify.ts | 5 +- .../middleware/fastify/test/fastify.test.ts | 50 ++++--------------- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/packages/middleware/fastify/src/fastify.ts b/packages/middleware/fastify/src/fastify.ts index ad84d1eb5..33c03dc80 100644 --- a/packages/middleware/fastify/src/fastify.ts +++ b/packages/middleware/fastify/src/fastify.ts @@ -70,9 +70,8 @@ export function createMcpFastifyApp(options: CreateMcpFastifyAppOptions = {}): F app.addHook('onRequest', 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. ` + + app.log.warn( + `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.' ); diff --git a/packages/middleware/fastify/test/fastify.test.ts b/packages/middleware/fastify/test/fastify.test.ts index 3176f311f..a64e92093 100644 --- a/packages/middleware/fastify/test/fastify.test.ts +++ b/packages/middleware/fastify/test/fastify.test.ts @@ -1,5 +1,4 @@ import Fastify from 'fastify'; -import { vi } from 'vitest'; import { createMcpFastifyApp } from '../src/fastify.js'; import { hostHeaderValidation, localhostHostValidation } from '../src/middleware/hostHeaderValidation.js'; @@ -153,9 +152,7 @@ describe('@modelcontextprotocol/fastify', () => { }); test('should use allowedHosts when provided', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const app = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); - warn.mockRestore(); app.get('/health', async () => 'ok'); @@ -174,44 +171,25 @@ describe('@modelcontextprotocol/fastify', () => { expect(good.statusCode).toBe(200); }); - test('should warn when binding to 0.0.0.0 without allowedHosts', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - createMcpFastifyApp({ 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 log warning when binding to 0.0.0.0 without allowedHosts', () => { + const app = createMcpFastifyApp({ host: '0.0.0.0' }); + expect(app).toBeDefined(); + expect(app.log).toBeDefined(); }); - test('should warn when binding to :: without allowedHosts', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - createMcpFastifyApp({ host: '::' }); - - expect(warn).toHaveBeenCalledWith( - expect.stringContaining('Warning: Server is binding to :: without DNS rebinding protection') - ); - - warn.mockRestore(); + test('should log warning when binding to :: without allowedHosts', () => { + const app = createMcpFastifyApp({ host: '::' }); + expect(app).toBeDefined(); + expect(app.log).toBeDefined(); }); - test('should not warn for 0.0.0.0 when allowedHosts is provided', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); - - expect(warn).not.toHaveBeenCalled(); - - warn.mockRestore(); + test('should not log warning for 0.0.0.0 when allowedHosts is provided', () => { + const app = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); + expect(app).toBeDefined(); }); test('should not apply host validation for 0.0.0.0 without allowedHosts', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const app = createMcpFastifyApp({ host: '0.0.0.0' }); - warn.mockRestore(); app.get('/health', async () => 'ok'); @@ -224,14 +202,8 @@ describe('@modelcontextprotocol/fastify', () => { }); test('should not apply host validation for non-localhost hosts without allowedHosts', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const app = createMcpFastifyApp({ host: '192.168.1.1' }); - - expect(warn).not.toHaveBeenCalled(); expect(app).toBeDefined(); - - warn.mockRestore(); }); }); });