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>
This commit is contained in:
parent
5f32a90f0d
commit
81e4b2a412
13 changed files with 937 additions and 1 deletions
70
packages/middleware/fastify/README.md
Normal file
70
packages/middleware/fastify/README.md
Normal file
|
|
@ -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`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/packages/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
packages/middleware/fastify/eslint.config.mjs
Normal file
12
packages/middleware/fastify/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// @ts-check
|
||||
|
||||
import baseConfig from '@modelcontextprotocol/eslint-config';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
settings: {
|
||||
'import/internal-regex': '^@modelcontextprotocol/(server|core)'
|
||||
}
|
||||
}
|
||||
];
|
||||
67
packages/middleware/fastify/package.json
Normal file
67
packages/middleware/fastify/package.json
Normal file
|
|
@ -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
packages/middleware/fastify/src/fastify.examples.ts
Normal file
41
packages/middleware/fastify/src/fastify.examples.ts
Normal file
|
|
@ -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
packages/middleware/fastify/src/fastify.ts
Normal file
82
packages/middleware/fastify/src/fastify.ts
Normal file
|
|
@ -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
packages/middleware/fastify/src/index.ts
Normal file
2
packages/middleware/fastify/src/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './fastify.js';
|
||||
export * from './middleware/hostHeaderValidation.js';
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
209
packages/middleware/fastify/test/fastify.test.ts
Normal file
209
packages/middleware/fastify/test/fastify.test.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import Fastify from 'fastify';
|
||||
|
||||
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 app = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] });
|
||||
|
||||
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 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 log warning when binding to :: without allowedHosts', () => {
|
||||
const app = createMcpFastifyApp({ host: '::' });
|
||||
expect(app).toBeDefined();
|
||||
expect(app.log).toBeDefined();
|
||||
});
|
||||
|
||||
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 app = createMcpFastifyApp({ host: '0.0.0.0' });
|
||||
|
||||
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 app = createMcpFastifyApp({ host: '192.168.1.1' });
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
15
packages/middleware/fastify/tsconfig.json
Normal file
15
packages/middleware/fastify/tsconfig.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
21
packages/middleware/fastify/tsdown.config.ts
Normal file
21
packages/middleware/fastify/tsdown.config.ts
Normal file
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
10
packages/middleware/fastify/typedoc.json
Normal file
10
packages/middleware/fastify/typedoc.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPoints": ["src"],
|
||||
"entryPointStrategy": "expand",
|
||||
"exclude": ["**/*.test.ts"],
|
||||
"navigation": {
|
||||
"includeGroups": true,
|
||||
"includeCategories": true
|
||||
}
|
||||
}
|
||||
3
packages/middleware/fastify/vitest.config.js
Normal file
3
packages/middleware/fastify/vitest.config.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import baseConfig from '@modelcontextprotocol/vitest-config';
|
||||
|
||||
export default baseConfig;
|
||||
Loading…
Reference in a new issue