Skip to content

Commit 81e4b2a

Browse files
andyflemingfelixweinbergerKKonstantinov
authored
Adds Fastify Middleware for v2 (#1536)
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com> Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com> Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
1 parent 5f32a90 commit 81e4b2a

File tree

17 files changed

+937
-1
lines changed

17 files changed

+937
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/fastify': minor
3+
---
4+
5+
Add Fastify middleware adapter for MCP servers, following the same pattern as the Express and Hono adapters.

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ jobs:
4040
- name: Publish preview packages
4141
run:
4242
pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
43-
'./packages/middleware/express' './packages/middleware/hono' './packages/middleware/node'
43+
'./packages/middleware/express' './packages/middleware/fastify' './packages/middleware/hono' './packages/middleware/node'
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# `@modelcontextprotocol/fastify`
2+
3+
Fastify adapters for the MCP TypeScript server SDK.
4+
5+
This package is a thin Fastify integration layer for [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/server).
6+
7+
It does **not** implement MCP itself. Instead, it helps you:
8+
9+
- create a Fastify app with sensible defaults for MCP servers
10+
- add DNS rebinding protection via Host header validation (recommended for localhost servers)
11+
12+
## Install
13+
14+
```bash
15+
npm install @modelcontextprotocol/server @modelcontextprotocol/fastify fastify
16+
17+
# For MCP Streamable HTTP over Node.js (IncomingMessage/ServerResponse):
18+
npm install @modelcontextprotocol/node
19+
```
20+
21+
## Exports
22+
23+
- `createMcpFastifyApp(options?)`
24+
- `hostHeaderValidation(allowedHostnames)`
25+
- `localhostHostValidation()`
26+
27+
## Usage
28+
29+
### Create a Fastify app (localhost DNS rebinding protection by default)
30+
31+
```ts
32+
import { createMcpFastifyApp } from '@modelcontextprotocol/fastify';
33+
34+
const app = createMcpFastifyApp(); // default host is 127.0.0.1; protection enabled
35+
```
36+
37+
### Streamable HTTP endpoint (Fastify)
38+
39+
```ts
40+
import { createMcpFastifyApp } from '@modelcontextprotocol/fastify';
41+
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
42+
import { McpServer } from '@modelcontextprotocol/server';
43+
44+
const app = createMcpFastifyApp();
45+
const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
46+
47+
app.post('/mcp', async (request, reply) => {
48+
// Stateless example: create a transport per request.
49+
// For stateful mode (sessions), keep a transport instance around and reuse it.
50+
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
51+
await mcpServer.connect(transport);
52+
53+
// Clean up when the client closes the connection (e.g. during SSE streaming).
54+
reply.raw.on('close', () => {
55+
transport.close();
56+
});
57+
58+
await transport.handleRequest(request.raw, reply.raw, request.body);
59+
});
60+
```
61+
62+
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.
63+
64+
### Host header validation (DNS rebinding protection)
65+
66+
```ts
67+
import { hostHeaderValidation } from '@modelcontextprotocol/fastify';
68+
69+
app.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));
70+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @ts-check
2+
3+
import baseConfig from '@modelcontextprotocol/eslint-config';
4+
5+
export default [
6+
...baseConfig,
7+
{
8+
settings: {
9+
'import/internal-regex': '^@modelcontextprotocol/(server|core)'
10+
}
11+
}
12+
];
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"name": "@modelcontextprotocol/fastify",
3+
"private": false,
4+
"version": "2.0.0-alpha.0",
5+
"description": "Fastify adapters for the Model Context Protocol TypeScript server SDK - Fastify middleware",
6+
"license": "MIT",
7+
"author": "Anthropic, PBC (https://anthropic.com)",
8+
"homepage": "https://modelcontextprotocol.io",
9+
"bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
10+
"type": "module",
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
14+
},
15+
"engines": {
16+
"node": ">=20",
17+
"pnpm": ">=10.24.0"
18+
},
19+
"packageManager": "pnpm@10.24.0",
20+
"keywords": [
21+
"modelcontextprotocol",
22+
"mcp",
23+
"fastify",
24+
"middleware"
25+
],
26+
"exports": {
27+
".": {
28+
"types": "./dist/index.d.mts",
29+
"import": "./dist/index.mjs"
30+
}
31+
},
32+
"files": [
33+
"dist"
34+
],
35+
"scripts": {
36+
"typecheck": "tsgo -p tsconfig.json --noEmit",
37+
"build": "tsdown",
38+
"build:watch": "tsdown --watch",
39+
"prepack": "npm run build",
40+
"lint": "eslint src/ && prettier --ignore-path ../../../.prettierignore --check .",
41+
"lint:fix": "eslint src/ --fix && prettier --ignore-path ../../../.prettierignore --write .",
42+
"check": "npm run typecheck && npm run lint",
43+
"test": "vitest run",
44+
"test:watch": "vitest"
45+
},
46+
"dependencies": {},
47+
"peerDependencies": {
48+
"@modelcontextprotocol/server": "workspace:^",
49+
"fastify": "catalog:runtimeServerOnly"
50+
},
51+
"devDependencies": {
52+
"@modelcontextprotocol/server": "workspace:^",
53+
"@modelcontextprotocol/tsconfig": "workspace:^",
54+
"@modelcontextprotocol/vitest-config": "workspace:^",
55+
"@modelcontextprotocol/eslint-config": "workspace:^",
56+
"@eslint/js": "catalog:devTools",
57+
"@typescript/native-preview": "catalog:devTools",
58+
"eslint": "catalog:devTools",
59+
"eslint-config-prettier": "catalog:devTools",
60+
"eslint-plugin-n": "catalog:devTools",
61+
"prettier": "catalog:devTools",
62+
"tsdown": "catalog:devTools",
63+
"typescript": "catalog:devTools",
64+
"typescript-eslint": "catalog:devTools",
65+
"vitest": "catalog:devTools"
66+
}
67+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Type-checked examples for `fastify.ts`.
3+
*
4+
* These examples are synced into JSDoc comments via the sync-snippets script.
5+
* Each function's region markers define the code snippet that appears in the docs.
6+
*
7+
* @module
8+
*/
9+
10+
import { createMcpFastifyApp } from './fastify.js';
11+
12+
/**
13+
* Example: Basic usage with default DNS rebinding protection.
14+
*/
15+
function createMcpFastifyApp_default() {
16+
//#region createMcpFastifyApp_default
17+
const app = createMcpFastifyApp();
18+
//#endregion createMcpFastifyApp_default
19+
return app;
20+
}
21+
22+
/**
23+
* Example: Custom host binding with and without DNS rebinding protection.
24+
*/
25+
function createMcpFastifyApp_customHost() {
26+
//#region createMcpFastifyApp_customHost
27+
const appOpen = createMcpFastifyApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection
28+
const appLocal = createMcpFastifyApp({ host: 'localhost' }); // DNS rebinding protection enabled
29+
//#endregion createMcpFastifyApp_customHost
30+
return { appOpen, appLocal };
31+
}
32+
33+
/**
34+
* Example: Custom allowed hosts for non-localhost binding.
35+
*/
36+
function createMcpFastifyApp_allowedHosts() {
37+
//#region createMcpFastifyApp_allowedHosts
38+
const app = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] });
39+
//#endregion createMcpFastifyApp_allowedHosts
40+
return app;
41+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { FastifyInstance } from 'fastify';
2+
import Fastify from 'fastify';
3+
4+
import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js';
5+
6+
/**
7+
* Options for creating an MCP Fastify application.
8+
*/
9+
export interface CreateMcpFastifyAppOptions {
10+
/**
11+
* The hostname to bind to. Defaults to `'127.0.0.1'`.
12+
* When set to `'127.0.0.1'`, `'localhost'`, or `'::1'`, DNS rebinding protection is automatically enabled.
13+
*/
14+
host?: string;
15+
16+
/**
17+
* List of allowed hostnames for DNS rebinding protection.
18+
* If provided, host header validation will be applied using this list.
19+
* For IPv6, provide addresses with brackets (e.g., `'[::1]'`).
20+
*
21+
* This is useful when binding to `'0.0.0.0'` or `'::'` but still wanting
22+
* to restrict which hostnames are allowed.
23+
*/
24+
allowedHosts?: string[];
25+
}
26+
27+
/**
28+
* Creates a Fastify application pre-configured for MCP servers.
29+
*
30+
* When the host is `'127.0.0.1'`, `'localhost'`, or `'::1'` (the default is `'127.0.0.1'`),
31+
* DNS rebinding protection is automatically applied via an onRequest hook to protect against
32+
* DNS rebinding attacks on localhost servers.
33+
*
34+
* Fastify parses JSON request bodies by default, so no additional middleware is required
35+
* for MCP Streamable HTTP endpoints.
36+
*
37+
* @param options - Configuration options
38+
* @returns A configured Fastify application
39+
*
40+
* @example Basic usage - defaults to 127.0.0.1 with DNS rebinding protection
41+
* ```ts source="./fastify.examples.ts#createMcpFastifyApp_default"
42+
* const app = createMcpFastifyApp();
43+
* ```
44+
*
45+
* @example Custom host - DNS rebinding protection only applied for localhost hosts
46+
* ```ts source="./fastify.examples.ts#createMcpFastifyApp_customHost"
47+
* const appOpen = createMcpFastifyApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection
48+
* const appLocal = createMcpFastifyApp({ host: 'localhost' }); // DNS rebinding protection enabled
49+
* ```
50+
*
51+
* @example Custom allowed hosts for non-localhost binding
52+
* ```ts source="./fastify.examples.ts#createMcpFastifyApp_allowedHosts"
53+
* const app = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] });
54+
* ```
55+
*/
56+
export function createMcpFastifyApp(options: CreateMcpFastifyAppOptions = {}): FastifyInstance {
57+
const { host = '127.0.0.1', allowedHosts } = options;
58+
59+
const app = Fastify();
60+
61+
// Fastify parses JSON by default - no middleware needed
62+
63+
// If allowedHosts is explicitly provided, use that for validation
64+
if (allowedHosts) {
65+
app.addHook('onRequest', hostHeaderValidation(allowedHosts));
66+
} else {
67+
// Apply DNS rebinding protection automatically for localhost hosts
68+
const localhostHosts = ['127.0.0.1', 'localhost', '::1'];
69+
if (localhostHosts.includes(host)) {
70+
app.addHook('onRequest', localhostHostValidation());
71+
} else if (host === '0.0.0.0' || host === '::') {
72+
// Warn when binding to all interfaces without DNS rebinding protection
73+
app.log.warn(
74+
`Server is binding to ${host} without DNS rebinding protection. ` +
75+
'Consider using the allowedHosts option to restrict allowed hosts, ' +
76+
'or use authentication to protect your server.'
77+
);
78+
}
79+
}
80+
81+
return app;
82+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './fastify.js';
2+
export * from './middleware/hostHeaderValidation.js';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Type-checked examples for `hostHeaderValidation.ts`.
3+
*
4+
* These examples are synced into JSDoc comments via the sync-snippets script.
5+
* Each function's region markers define the code snippet that appears in the docs.
6+
*
7+
* @module
8+
*/
9+
10+
import type { FastifyInstance } from 'fastify';
11+
12+
import { hostHeaderValidation, localhostHostValidation } from './hostHeaderValidation.js';
13+
14+
/**
15+
* Example: Using hostHeaderValidation hook with custom allowed hosts.
16+
*/
17+
function hostHeaderValidation_basicUsage(app: FastifyInstance) {
18+
//#region hostHeaderValidation_basicUsage
19+
app.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));
20+
//#endregion hostHeaderValidation_basicUsage
21+
}
22+
23+
/**
24+
* Example: Using localhostHostValidation convenience hook.
25+
*/
26+
function localhostHostValidation_basicUsage(app: FastifyInstance) {
27+
//#region localhostHostValidation_basicUsage
28+
app.addHook('onRequest', localhostHostValidation());
29+
//#endregion localhostHostValidation_basicUsage
30+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { localhostAllowedHostnames, validateHostHeader } from '@modelcontextprotocol/server';
2+
import type { FastifyReply, FastifyRequest } from 'fastify';
3+
4+
/**
5+
* Fastify onRequest hook for DNS rebinding protection.
6+
* Validates `Host` header hostname (port-agnostic) against an allowed list.
7+
*
8+
* This is particularly important for servers without authorization or HTTPS,
9+
* such as localhost servers or development servers. DNS rebinding attacks can
10+
* bypass same-origin policy by manipulating DNS to point a domain to a
11+
* localhost address, allowing malicious websites to access your local server.
12+
*
13+
* @param allowedHostnames - List of allowed hostnames (without ports).
14+
* For IPv6, provide the address with brackets (e.g., `[::1]`).
15+
* @returns Fastify onRequest hook handler
16+
*
17+
* @example
18+
* ```ts source="./hostHeaderValidation.examples.ts#hostHeaderValidation_basicUsage"
19+
* app.addHook('onRequest', hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']));
20+
* ```
21+
*/
22+
export function hostHeaderValidation(allowedHostnames: string[]) {
23+
return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
24+
const result = validateHostHeader(request.headers.host, allowedHostnames);
25+
if (!result.ok) {
26+
await reply.code(403).send({
27+
jsonrpc: '2.0',
28+
error: {
29+
code: -32_000,
30+
message: result.message
31+
},
32+
id: null
33+
});
34+
}
35+
};
36+
}
37+
38+
/**
39+
* Convenience hook for localhost DNS rebinding protection.
40+
* Allows only `localhost`, `127.0.0.1`, and `[::1]` (IPv6 localhost) hostnames.
41+
*
42+
* @example
43+
* ```ts source="./hostHeaderValidation.examples.ts#localhostHostValidation_basicUsage"
44+
* app.addHook('onRequest', localhostHostValidation());
45+
* ```
46+
*/
47+
export function localhostHostValidation() {
48+
return hostHeaderValidation(localhostAllowedHostnames());
49+
}

0 commit comments

Comments
 (0)