From 60a869fe06c9fb1f5948f9fa126a575f5c776188 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 11 Sep 2025 15:53:33 +0200 Subject: [PATCH] Use @fastify/see take 2 Signed-off-by: Matteo Collina --- package-lock.json | 58 +++++++++---------------------- package.json | 1 + src/routes/mcp.ts | 85 ++++++++++++++++++++++------------------------ test/index.test.ts | 2 +- 4 files changed, 59 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ff3073..2169a9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@fastify/cors": "^11.1.0", "@fastify/jwt": "^9.1.0", + "@fastify/sse": "^0.3.0", "@fastify/type-provider-typebox": "^5.2.0", "fast-jwt": "^6.0.2", "fastify-plugin": "^5.0.1", @@ -232,7 +233,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz", "integrity": "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==", - "dev": true, "funding": [ { "type": "github", @@ -290,7 +290,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", - "dev": true, "funding": [ { "type": "github", @@ -310,7 +309,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", - "dev": true, "license": "MIT" }, "node_modules/@fastify/jwt": { @@ -355,7 +353,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", - "dev": true, "funding": [ { "type": "github", @@ -413,13 +410,27 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", - "dev": true, "license": "MIT", "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, + "node_modules/@fastify/sse": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@fastify/sse/-/sse-0.3.0.tgz", + "integrity": "sha512-y/B52CuqiMYE8ElqxTGQ26+8dTanjJ1c3qukKWStFMG6am1cArG++Go7rI3927qHIRPZ0kuv4Mm2t4j7wO4bTA==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "fastify": "^5.x" + } + }, "node_modules/@fastify/type-provider-typebox": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@fastify/type-provider-typebox/-/type-provider-typebox-5.2.0.tgz", @@ -2025,7 +2036,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "dev": true, "license": "MIT" }, "node_modules/accepts": { @@ -2082,7 +2092,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2099,7 +2108,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -2330,7 +2338,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -2356,7 +2363,6 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", - "dev": true, "license": "MIT", "dependencies": { "@fastify/error": "^4.0.0", @@ -2761,7 +2767,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3003,7 +3008,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3911,14 +3915,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3962,7 +3964,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz", "integrity": "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==", - "dev": true, "funding": [ { "type": "github", @@ -4009,7 +4010,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "dev": true, "license": "MIT", "dependencies": { "fast-decode-uri-component": "^1.0.1" @@ -4019,7 +4019,6 @@ "version": "3.5.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4036,7 +4035,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, "funding": [ { "type": "github", @@ -4065,7 +4063,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.4.0.tgz", "integrity": "sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw==", - "dev": true, "funding": [ { "type": "github", @@ -4193,7 +4190,6 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.3.0.tgz", "integrity": "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4777,7 +4773,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -5325,7 +5320,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", "integrity": "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==", - "dev": true, "funding": [ { "type": "github", @@ -5345,7 +5339,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -5410,7 +5403,6 @@ "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", - "dev": true, "funding": [ { "type": "github", @@ -5432,7 +5424,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", - "dev": true, "funding": [ { "type": "github", @@ -5913,7 +5904,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -6123,7 +6113,6 @@ "version": "9.7.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz", "integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==", - "dev": true, "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0", @@ -6146,7 +6135,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "dev": true, "license": "MIT", "dependencies": { "split2": "^4.0.0" @@ -6188,7 +6176,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "dev": true, "license": "MIT" }, "node_modules/pkce-challenge": { @@ -6235,7 +6222,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "dev": true, "funding": [ { "type": "github", @@ -6355,7 +6341,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "dev": true, "license": "MIT" }, "node_modules/range-parser": { @@ -6505,7 +6490,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 12.13.0" @@ -6590,7 +6574,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6638,7 +6621,6 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -6658,7 +6640,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, "license": "MIT" }, "node_modules/router": { @@ -6804,7 +6785,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", - "dev": true, "funding": [ { "type": "github", @@ -6849,7 +6829,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz", "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==", - "dev": true, "funding": [ { "type": "github", @@ -6866,7 +6845,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6994,7 +6972,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "dev": true, "license": "MIT" }, "node_modules/set-function-length": { @@ -7169,7 +7146,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", - "dev": true, "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0" @@ -7190,7 +7166,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, "license": "ISC", "engines": { "node": ">= 10.x" @@ -7446,7 +7421,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "dev": true, "license": "MIT", "dependencies": { "real-require": "^0.2.0" diff --git a/package.json b/package.json index dbd6f48..52d3ba0 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dependencies": { "@fastify/cors": "^11.1.0", "@fastify/jwt": "^9.1.0", + "@fastify/sse": "^0.3.0", "@fastify/type-provider-typebox": "^5.2.0", "fast-jwt": "^6.0.2", "fastify-plugin": "^5.0.1", diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index 37fe54e..f31fb42 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -1,6 +1,7 @@ import { randomUUID } from 'crypto' import type { FastifyRequest, FastifyReply, FastifyPluginAsync } from 'fastify' import fp from 'fastify-plugin' +import fastifySSE from '@fastify/sse' import type { JSONRPCMessage } from '../schema.ts' import { JSONRPC_VERSION, INTERNAL_ERROR } from '../schema.ts' import type { MCPPluginOptions, MCPTool, MCPResource, MCPPrompt } from '../types.ts' @@ -25,6 +26,11 @@ interface MCPPubSubRoutesOptions { const mcpPubSubRoutesPlugin: FastifyPluginAsync = async (app, options) => { const { enableSSE, opts, capabilities, serverInfo, tools, resources, prompts, sessionStore, messageBroker, localStreams } = options + // Register @fastify/sse if SSE is enabled + if (enableSSE) { + await app.register(fastifySSE as any) + } + async function createSSESession (): Promise { const sessionId = randomUUID() const session: SessionMetadata = { @@ -76,7 +82,6 @@ const mcpPubSubRoutesPlugin: FastifyPluginAsync = async if (!session) return const eventId = (++session.eventId).toString() - const sseEvent = `id: ${eventId}\ndata: ${JSON.stringify(message)}\n\n` session.lastEventId = eventId session.lastActivity = new Date() @@ -87,7 +92,10 @@ const mcpPubSubRoutesPlugin: FastifyPluginAsync = async const deadStreams = new Set() for (const stream of streams) { try { - stream.raw.write(sseEvent) + await stream.sse.send({ + id: eventId, + data: message + }) } catch (error) { app.log.error({ err: error }, 'Failed to write SSE event') deadStreams.add(stream) @@ -114,9 +122,11 @@ const mcpPubSubRoutesPlugin: FastifyPluginAsync = async const messagesToReplay = await sessionStore.getMessagesFrom(sessionId, lastEventId) for (const entry of messagesToReplay) { - const sseEvent = `id: ${entry.eventId}\ndata: ${JSON.stringify(entry.message)}\n\n` try { - stream.raw.write(sseEvent) + await stream.sse.send({ + id: entry.eventId, + data: entry.message + }) } catch (error) { app.log.error({ err: error }, 'Failed to replay SSE event') break @@ -208,7 +218,7 @@ const mcpPubSubRoutesPlugin: FastifyPluginAsync = async }) // GET endpoint for server-initiated communication via SSE - app.get('/mcp', async (request: FastifyRequest, reply: FastifyReply) => { + app.get('/mcp', { sse: enableSSE }, async (request: FastifyRequest, reply: FastifyReply) => { if (!enableSSE) { reply.type('application/json').code(405).send({ error: 'Method Not Allowed: SSE not enabled' }) return @@ -233,15 +243,6 @@ const mcpPubSubRoutesPlugin: FastifyPluginAsync = async request.log.info({ sessionId }, 'Handling SSE request') - // We are opting out of Fastify proper - reply.hijack() - - const raw = reply.raw - - // Set up SSE stream - raw.setHeader('Content-type', 'text/event-stream') - raw.setHeader('Cache-Control', 'no-cache') - let session: SessionMetadata if (sessionId) { const existingSession = await sessionStore.get(sessionId) @@ -249,14 +250,15 @@ const mcpPubSubRoutesPlugin: FastifyPluginAsync = async session = existingSession } else { session = await createSSESession() - raw.setHeader('Mcp-Session-Id', session.id) + reply.header('Mcp-Session-Id', session.id) } } else { session = await createSSESession() - raw.setHeader('Mcp-Session-Id', session.id) + reply.header('Mcp-Session-Id', session.id) } - raw.writeHead(200) + // Initialize SSE connection - headers are sent automatically on first message + reply.sse.keepAlive() let streams = localStreams.get(session.id) if (!streams) { @@ -278,33 +280,18 @@ const mcpPubSubRoutesPlugin: FastifyPluginAsync = async await replayMessagesFromEventId(session.id, lastEventId, reply) } - // Handle connection close - reply.raw.on('close', () => { - const streams = localStreams.get(session.id) - if (streams) { - streams.delete(reply) - app.log.info({ - sessionId: session.id, - remainingStreams: streams.size - }, 'SSE connection closed') - - if (streams.size === 0) { - app.log.info({ - sessionId: session.id - }, 'Last SSE stream closed, cleaning up session') - localStreams.delete(session.id) - messageBroker.unsubscribe(`mcp/session/${session.id}/message`) - } - } - }) - - // Send initial heartbeat - reply.raw.write(': heartbeat\n\n') - // Keep connection alive with periodic heartbeats - const heartbeatInterval = setInterval(() => { + const heartbeatInterval = setInterval(async () => { try { - reply.raw.write(': heartbeat\n\n') + if (reply.sse.isConnected) { + await reply.sse.send({ event: 'heartbeat', data: 'heartbeat' }) + } else { + clearInterval(heartbeatInterval) + const streams = localStreams.get(session.id) + if (streams) { + streams.delete(reply) + } + } } catch (error) { clearInterval(heartbeatInterval) const streams = localStreams.get(session.id) @@ -315,11 +302,21 @@ const mcpPubSubRoutesPlugin: FastifyPluginAsync = async }, 30000) // 30 second heartbeat heartbeatInterval.unref() - reply.raw.on('close', () => { + // Handle connection close using @fastify/sse API + reply.sse.onClose(() => { app.log.info({ sessionId: session.id - }, 'SSE heartbeat connection closed') + }, 'SSE connection closed') clearInterval(heartbeatInterval) + + const streams = localStreams.get(session.id) + if (streams) { + streams.delete(reply) + if (streams.size === 0) { + localStreams.delete(session.id) + messageBroker.unsubscribe(`mcp/session/${session.id}/message`) + } + } }) } catch (error) { app.log.error({ err: error }, 'Error setting up SSE stream') diff --git a/test/index.test.ts b/test/index.test.ts index d947f54..35a970f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -370,7 +370,7 @@ describe('MCP Fastify Plugin', () => { describe('SSE Support', () => { test('should handle GET request with SSE support', async (t: TestContext) => { - const app = Fastify() + const app = Fastify({ logger: true }) t.after(() => app.close()) await app.register(mcpPlugin, { enableSSE: true })