Skip to content
Merged
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
41 changes: 24 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,42 @@
# fastify-mcp

Integrate [Model Context Protocol](https://modelcontextprotocol.io/) servers with your [Fastify](https://www.fastify.dev) app over [SSE](https://en.wikipedia.org/wiki/Server-sent_events) connections.
Integrate [Model Context Protocol](https://modelcontextprotocol.io/) servers with your [Fastify](https://www.fastify.dev) app.

Supports the [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) as well as the legacy [HTTP+SSE transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse).

## Usage

First, define your MCP server.

```ts
const mcpServer = new McpServer({
name: "...",
version: "...",
});
function createServer() {
const mcpServer = new McpServer({
name: "...",
version: "...",
});

mcpServer.tool("...");
mcpServer.resource("...");
mcpServer.tool("...");
mcpServer.resource("...");

return mcpServer.server;
}
```

Create a Fastify app and register the plugin.

```ts
import { fastify } from "fastify";
import { fastifyMCPSSE } from "fastify-mcp";
import { streamableHttp } from "fastify-mcp";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = fastify();

app.register(fastifyMCPSSE, {
// Pass the lower-level Server class to the plugin
server: mcpServer.server,
app.register(streamableHttp, {
// Set to `true` if you want a stateful server
stateful: false,
mcpEndpoint: "/mcp",
sessions: new Sessions<StreamableHTTPServerTransport>()
createServer,
});

app.listen({ port: 8080 });
Expand All @@ -46,12 +56,9 @@ yarn add fastify-mcp

## Session Management

The official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) does not support managing multiple SSE sessions out of the box, and therefore it's the host server's responsibility to do so.

This package uses an in-memory mapping of each active session against its session ID to manage multiple sessions.
The official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) does not support managing multiple sessions out of the box, and therefore it's the host server's responsibility to do so.

- The ID of a session is generated by `SSEServerTransport` from the official SDK.
- The session is removed from memory whenever the response stream is closed, either by the server or due to a client disconnection.
This package uses an in-memory mapping of each active session against its session ID to manage multiple sessions, as recommended by the MCP SDK examples.

### Session Events

Expand All @@ -62,7 +69,7 @@ The `Sessions` class emits the following events:
- `error`: Emitted when an asynchronous event handler throws an error.

```ts
const sessions = new Sessions();
const sessions = new Sessions<StreamableHTTPServerTransport>();

sessions.on("connected", (sessionId) => {
console.log(`Session ${sessionId} connected`);
Expand Down
38 changes: 38 additions & 0 deletions examples/stateful-streamable-http-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import fastify from "fastify";
import { Sessions, streamableHttp } from "../src";

const app = fastify();

app.register(streamableHttp, {
stateful: true,
mcpEndpoint: "/mcp",
createServer: () => {
const mcpServer = new McpServer({
name: "stateful-streamable-http-server",
version: "0.0.1",
});

mcpServer.tool("greet", () => {
return {
content: [{ type: "text", text: "Hello, world!" }],
};
});

return mcpServer.server;
},
sessions: new Sessions<StreamableHTTPServerTransport>(),
});

app
.listen({
port: 3000,
})
.then(() => {
console.log("Server is running on port 3000");
})
.catch((err) => {
console.error(err);
process.exit(1);
});
47 changes: 47 additions & 0 deletions examples/stateless-streamable-http-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { fastify } from "fastify";
import { streamableHttp } from "../src";

const app = fastify({
logger: {
level: "error",
transport: {
target: "pino-pretty",
options: {
translateTime: "HH:MM:ss Z",
ignore: "pid,hostname",
},
},
},
});

app.register(streamableHttp, {
stateful: false,
mcpEndpoint: "/mcp",
createServer: () => {
const mcpServer = new McpServer({
name: "stateless-streamable-http-server",
version: "0.0.1",
});

mcpServer.tool("greet", () => {
return {
content: [{ type: "text", text: "Hello, world!" }],
};
});

return mcpServer.server;
},
});

app
.listen({
port: 3000,
})
.then(() => {
console.log("Server is running on port 3000");
})
.catch((err) => {
console.error(err);
process.exit(1);
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@
"eslint-config-prettier": "^10.0.2",
"globals": "^16.0.0",
"jest": "^29.7.0",
"pino-pretty": "^13.0.0",
"prettier": "3.5.3",
"ts-jest": "^29.2.6",
"ts-node": "^10.9.2",
"typescript": "^5.8.2",
"typescript-eslint": "^8.25.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.0",
"@modelcontextprotocol/sdk": "^1.11.0",
"fastify": "^5.2.1"
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { fastifyMCPSSE } from "./mcp-sse-plugin";
export { Sessions } from "./session-storage";
export { streamableHttp } from "./streamable-http";
3 changes: 2 additions & 1 deletion src/mcp-sse-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import fastify from "fastify";
import { randomUUID } from "node:crypto";
import { Readable } from "node:stream";
Expand Down Expand Up @@ -84,7 +85,7 @@ describe(fastifyMCPSSE.name, () => {
version: "1.0.0",
});

const sessions = new Sessions();
const sessions = new Sessions<SSEServerTransport>();
app.register(fastifyMCPSSE, {
server: mcpServer,
sessions,
Expand Down
11 changes: 9 additions & 2 deletions src/mcp-sse-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@ import { Sessions } from "./session-storage";

type MCPSSEPluginOptions = {
server: Server;
sessions?: Sessions;
sessions?: Sessions<SSEServerTransport>;
sseEndpoint?: string;
messagesEndpoint?: string;
};

/**
* A plugin to run MCP servers using HTTP with SSE Transport over Fastify.
*
* @deprecated Use {@link streamableHttp} instead. The HTTP with SSE Transport
* has been deprecated from MCP protocol version 2025-03-26 onwards. Consider
* migrating to the Streamable HTTP Transport instead.
*/
export const fastifyMCPSSE: FastifyPluginCallback<MCPSSEPluginOptions> = (
fastify,
options,
done,
) => {
const {
server,
sessions = new Sessions(),
sessions = new Sessions<SSEServerTransport>(),
sseEndpoint = "/sse",
messagesEndpoint = "/messages",
} = options;
Expand Down
12 changes: 6 additions & 6 deletions src/session-storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport";
import { EventEmitter } from "node:events";

type SessionEvents = {
Expand All @@ -7,18 +7,18 @@ type SessionEvents = {
error: [unknown];
};

export class Sessions
export class Sessions<T extends Transport>
extends EventEmitter<SessionEvents>
implements Iterable<SSEServerTransport>
implements Iterable<T>
{
private readonly sessions: Map<string, SSEServerTransport>;
private readonly sessions: Map<string, T>;

constructor() {
super({ captureRejections: true });
this.sessions = new Map();
}

add = (id: string, transport: SSEServerTransport) => {
add = (id: string, transport: T) => {
if (this.sessions.has(id)) {
throw new Error("Session already exists");
}
Expand All @@ -32,7 +32,7 @@ export class Sessions
this.emit("terminated", id);
};

get = (id: string): SSEServerTransport | undefined => {
get = (id: string): T | undefined => {
return this.sessions.get(id);
};

Expand Down
Loading