Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions packages/middleware/fastify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# `@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 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 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
import { hostHeaderValidation } from '@modelcontextprotocol/fastify';

app.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));
```
12 changes: 12 additions & 0 deletions packages/middleware/fastify/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-check

import baseConfig from '@modelcontextprotocol/eslint-config';

export default [
...baseConfig,
{
settings: {
'import/internal-regex': '^@modelcontextprotocol/(server|core)'
}
}
];
67 changes: 67 additions & 0 deletions packages/middleware/fastify/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
41 changes: 41 additions & 0 deletions packages/middleware/fastify/src/fastify.examples.ts
Original file line number Diff line number Diff line change
@@ -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;
}
82 changes: 82 additions & 0 deletions packages/middleware/fastify/src/fastify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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
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.'
);
}
}

return app;
}
2 changes: 2 additions & 0 deletions packages/middleware/fastify/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './fastify.js';
export * from './middleware/hostHeaderValidation.js';
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<void> => {
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());
}
Loading
Loading