@nitrostack/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +80 -0
  3. package/dist/auth/api-key.d.ts +118 -0
  4. package/dist/auth/api-key.d.ts.map +1 -0
  5. package/dist/auth/api-key.js +168 -0
  6. package/dist/auth/api-key.js.map +1 -0
  7. package/dist/auth/client.d.ts +151 -0
  8. package/dist/auth/client.d.ts.map +1 -0
  9. package/dist/auth/client.js +330 -0
  10. package/dist/auth/client.js.map +1 -0
  11. package/dist/auth/index.d.ts +31 -0
  12. package/dist/auth/index.d.ts.map +1 -0
  13. package/dist/auth/index.js +46 -0
  14. package/dist/auth/index.js.map +1 -0
  15. package/dist/auth/middleware.d.ts +95 -0
  16. package/dist/auth/middleware.d.ts.map +1 -0
  17. package/dist/auth/middleware.js +260 -0
  18. package/dist/auth/middleware.js.map +1 -0
  19. package/dist/auth/pkce.d.ts +53 -0
  20. package/dist/auth/pkce.d.ts.map +1 -0
  21. package/dist/auth/pkce.js +105 -0
  22. package/dist/auth/pkce.js.map +1 -0
  23. package/dist/auth/quick-setup.d.ts +94 -0
  24. package/dist/auth/quick-setup.d.ts.map +1 -0
  25. package/dist/auth/quick-setup.js +210 -0
  26. package/dist/auth/quick-setup.js.map +1 -0
  27. package/dist/auth/secure-secret.d.ts +136 -0
  28. package/dist/auth/secure-secret.d.ts.map +1 -0
  29. package/dist/auth/secure-secret.js +182 -0
  30. package/dist/auth/secure-secret.js.map +1 -0
  31. package/dist/auth/server-integration.d.ts +97 -0
  32. package/dist/auth/server-integration.d.ts.map +1 -0
  33. package/dist/auth/server-integration.js +182 -0
  34. package/dist/auth/server-integration.js.map +1 -0
  35. package/dist/auth/server-metadata.d.ts +51 -0
  36. package/dist/auth/server-metadata.d.ts.map +1 -0
  37. package/dist/auth/server-metadata.js +106 -0
  38. package/dist/auth/server-metadata.js.map +1 -0
  39. package/dist/auth/simple-jwt.d.ts +174 -0
  40. package/dist/auth/simple-jwt.d.ts.map +1 -0
  41. package/dist/auth/simple-jwt.js +162 -0
  42. package/dist/auth/simple-jwt.js.map +1 -0
  43. package/dist/auth/token-store.d.ts +104 -0
  44. package/dist/auth/token-store.d.ts.map +1 -0
  45. package/dist/auth/token-store.js +205 -0
  46. package/dist/auth/token-store.js.map +1 -0
  47. package/dist/auth/token-validation.d.ts +59 -0
  48. package/dist/auth/token-validation.d.ts.map +1 -0
  49. package/dist/auth/token-validation.js +241 -0
  50. package/dist/auth/token-validation.js.map +1 -0
  51. package/dist/auth/types.d.ts +215 -0
  52. package/dist/auth/types.d.ts.map +1 -0
  53. package/dist/auth/types.js +6 -0
  54. package/dist/auth/types.js.map +1 -0
  55. package/dist/core/apikey-module.d.ts +69 -0
  56. package/dist/core/apikey-module.d.ts.map +1 -0
  57. package/dist/core/apikey-module.js +114 -0
  58. package/dist/core/apikey-module.js.map +1 -0
  59. package/dist/core/app-decorator.d.ts +59 -0
  60. package/dist/core/app-decorator.d.ts.map +1 -0
  61. package/dist/core/app-decorator.js +322 -0
  62. package/dist/core/app-decorator.js.map +1 -0
  63. package/dist/core/builders.d.ts +50 -0
  64. package/dist/core/builders.d.ts.map +1 -0
  65. package/dist/core/builders.js +139 -0
  66. package/dist/core/builders.js.map +1 -0
  67. package/dist/core/component.d.ts +111 -0
  68. package/dist/core/component.d.ts.map +1 -0
  69. package/dist/core/component.js +228 -0
  70. package/dist/core/component.js.map +1 -0
  71. package/dist/core/config-module.d.ts +62 -0
  72. package/dist/core/config-module.d.ts.map +1 -0
  73. package/dist/core/config-module.js +94 -0
  74. package/dist/core/config-module.js.map +1 -0
  75. package/dist/core/decorators/cache.decorator.d.ts +61 -0
  76. package/dist/core/decorators/cache.decorator.d.ts.map +1 -0
  77. package/dist/core/decorators/cache.decorator.js +115 -0
  78. package/dist/core/decorators/cache.decorator.js.map +1 -0
  79. package/dist/core/decorators/health-check.decorator.d.ts +80 -0
  80. package/dist/core/decorators/health-check.decorator.d.ts.map +1 -0
  81. package/dist/core/decorators/health-check.decorator.js +153 -0
  82. package/dist/core/decorators/health-check.decorator.js.map +1 -0
  83. package/dist/core/decorators/rate-limit.decorator.d.ts +63 -0
  84. package/dist/core/decorators/rate-limit.decorator.d.ts.map +1 -0
  85. package/dist/core/decorators/rate-limit.decorator.js +129 -0
  86. package/dist/core/decorators/rate-limit.decorator.js.map +1 -0
  87. package/dist/core/decorators.d.ts +190 -0
  88. package/dist/core/decorators.d.ts.map +1 -0
  89. package/dist/core/decorators.js +170 -0
  90. package/dist/core/decorators.js.map +1 -0
  91. package/dist/core/di/container.d.ts +64 -0
  92. package/dist/core/di/container.d.ts.map +1 -0
  93. package/dist/core/di/container.js +105 -0
  94. package/dist/core/di/container.js.map +1 -0
  95. package/dist/core/di/injectable.decorator.d.ts +62 -0
  96. package/dist/core/di/injectable.decorator.d.ts.map +1 -0
  97. package/dist/core/di/injectable.decorator.js +66 -0
  98. package/dist/core/di/injectable.decorator.js.map +1 -0
  99. package/dist/core/errors.d.ts +54 -0
  100. package/dist/core/errors.d.ts.map +1 -0
  101. package/dist/core/errors.js +87 -0
  102. package/dist/core/errors.js.map +1 -0
  103. package/dist/core/events/event-emitter.d.ts +50 -0
  104. package/dist/core/events/event-emitter.d.ts.map +1 -0
  105. package/dist/core/events/event-emitter.js +94 -0
  106. package/dist/core/events/event-emitter.js.map +1 -0
  107. package/dist/core/events/event.decorator.d.ts +48 -0
  108. package/dist/core/events/event.decorator.d.ts.map +1 -0
  109. package/dist/core/events/event.decorator.js +72 -0
  110. package/dist/core/events/event.decorator.js.map +1 -0
  111. package/dist/core/events/log-emitter.d.ts +14 -0
  112. package/dist/core/events/log-emitter.d.ts.map +1 -0
  113. package/dist/core/events/log-emitter.js +20 -0
  114. package/dist/core/events/log-emitter.js.map +1 -0
  115. package/dist/core/filters/exception-filter.decorator.d.ts +40 -0
  116. package/dist/core/filters/exception-filter.decorator.d.ts.map +1 -0
  117. package/dist/core/filters/exception-filter.decorator.js +54 -0
  118. package/dist/core/filters/exception-filter.decorator.js.map +1 -0
  119. package/dist/core/filters/exception-filter.interface.d.ts +39 -0
  120. package/dist/core/filters/exception-filter.interface.d.ts.map +1 -0
  121. package/dist/core/filters/exception-filter.interface.js +2 -0
  122. package/dist/core/filters/exception-filter.interface.js.map +1 -0
  123. package/dist/core/guards/apikey.guard.d.ts +22 -0
  124. package/dist/core/guards/apikey.guard.d.ts.map +1 -0
  125. package/dist/core/guards/apikey.guard.js +11 -0
  126. package/dist/core/guards/apikey.guard.js.map +1 -0
  127. package/dist/core/guards/guard.interface.d.ts +18 -0
  128. package/dist/core/guards/guard.interface.d.ts.map +1 -0
  129. package/dist/core/guards/guard.interface.js +2 -0
  130. package/dist/core/guards/guard.interface.js.map +1 -0
  131. package/dist/core/guards/jwt.guard.d.ts +18 -0
  132. package/dist/core/guards/jwt.guard.d.ts.map +1 -0
  133. package/dist/core/guards/jwt.guard.js +2 -0
  134. package/dist/core/guards/jwt.guard.js.map +1 -0
  135. package/dist/core/guards/oauth.guard.d.ts +35 -0
  136. package/dist/core/guards/oauth.guard.d.ts.map +1 -0
  137. package/dist/core/guards/oauth.guard.js +2 -0
  138. package/dist/core/guards/oauth.guard.js.map +1 -0
  139. package/dist/core/guards/use-guards.decorator.d.ts +25 -0
  140. package/dist/core/guards/use-guards.decorator.d.ts.map +1 -0
  141. package/dist/core/guards/use-guards.decorator.js +32 -0
  142. package/dist/core/guards/use-guards.decorator.js.map +1 -0
  143. package/dist/core/health/health-checks.resource.d.ts +14 -0
  144. package/dist/core/health/health-checks.resource.d.ts.map +1 -0
  145. package/dist/core/health/health-checks.resource.js +29 -0
  146. package/dist/core/health/health-checks.resource.js.map +1 -0
  147. package/dist/core/index.d.ts +57 -0
  148. package/dist/core/index.d.ts.map +1 -0
  149. package/dist/core/index.js +59 -0
  150. package/dist/core/index.js.map +1 -0
  151. package/dist/core/interceptors/interceptor.decorator.d.ts +37 -0
  152. package/dist/core/interceptors/interceptor.decorator.d.ts.map +1 -0
  153. package/dist/core/interceptors/interceptor.decorator.js +51 -0
  154. package/dist/core/interceptors/interceptor.decorator.js.map +1 -0
  155. package/dist/core/interceptors/interceptor.interface.d.ts +31 -0
  156. package/dist/core/interceptors/interceptor.interface.d.ts.map +1 -0
  157. package/dist/core/interceptors/interceptor.interface.js +2 -0
  158. package/dist/core/interceptors/interceptor.interface.js.map +1 -0
  159. package/dist/core/jwt-module.d.ts +51 -0
  160. package/dist/core/jwt-module.d.ts.map +1 -0
  161. package/dist/core/jwt-module.js +52 -0
  162. package/dist/core/jwt-module.js.map +1 -0
  163. package/dist/core/logger.d.ts +18 -0
  164. package/dist/core/logger.d.ts.map +1 -0
  165. package/dist/core/logger.js +53 -0
  166. package/dist/core/logger.js.map +1 -0
  167. package/dist/core/middleware/middleware.decorator.d.ts +39 -0
  168. package/dist/core/middleware/middleware.decorator.d.ts.map +1 -0
  169. package/dist/core/middleware/middleware.decorator.js +53 -0
  170. package/dist/core/middleware/middleware.decorator.js.map +1 -0
  171. package/dist/core/middleware/middleware.interface.d.ts +29 -0
  172. package/dist/core/middleware/middleware.interface.d.ts.map +1 -0
  173. package/dist/core/middleware/middleware.interface.js +2 -0
  174. package/dist/core/middleware/middleware.interface.js.map +1 -0
  175. package/dist/core/module.d.ts +93 -0
  176. package/dist/core/module.d.ts.map +1 -0
  177. package/dist/core/module.js +87 -0
  178. package/dist/core/module.js.map +1 -0
  179. package/dist/core/oauth-module.d.ts +123 -0
  180. package/dist/core/oauth-module.d.ts.map +1 -0
  181. package/dist/core/oauth-module.js +324 -0
  182. package/dist/core/oauth-module.js.map +1 -0
  183. package/dist/core/pipes/pipe.decorator.d.ts +64 -0
  184. package/dist/core/pipes/pipe.decorator.d.ts.map +1 -0
  185. package/dist/core/pipes/pipe.decorator.js +85 -0
  186. package/dist/core/pipes/pipe.decorator.js.map +1 -0
  187. package/dist/core/pipes/pipe.interface.d.ts +41 -0
  188. package/dist/core/pipes/pipe.interface.d.ts.map +1 -0
  189. package/dist/core/pipes/pipe.interface.js +2 -0
  190. package/dist/core/pipes/pipe.interface.js.map +1 -0
  191. package/dist/core/prompt.d.ts +46 -0
  192. package/dist/core/prompt.d.ts.map +1 -0
  193. package/dist/core/prompt.js +76 -0
  194. package/dist/core/prompt.js.map +1 -0
  195. package/dist/core/resource.d.ts +47 -0
  196. package/dist/core/resource.d.ts.map +1 -0
  197. package/dist/core/resource.js +90 -0
  198. package/dist/core/resource.js.map +1 -0
  199. package/dist/core/server.d.ts +129 -0
  200. package/dist/core/server.d.ts.map +1 -0
  201. package/dist/core/server.js +617 -0
  202. package/dist/core/server.js.map +1 -0
  203. package/dist/core/tool.d.ts +108 -0
  204. package/dist/core/tool.d.ts.map +1 -0
  205. package/dist/core/tool.js +241 -0
  206. package/dist/core/tool.js.map +1 -0
  207. package/dist/core/transports/discovery-http-server.d.ts +19 -0
  208. package/dist/core/transports/discovery-http-server.d.ts.map +1 -0
  209. package/dist/core/transports/discovery-http-server.js +54 -0
  210. package/dist/core/transports/discovery-http-server.js.map +1 -0
  211. package/dist/core/transports/http-server.d.ts +108 -0
  212. package/dist/core/transports/http-server.d.ts.map +1 -0
  213. package/dist/core/transports/http-server.js +293 -0
  214. package/dist/core/transports/http-server.js.map +1 -0
  215. package/dist/core/transports/streamable-http.d.ts +177 -0
  216. package/dist/core/transports/streamable-http.d.ts.map +1 -0
  217. package/dist/core/transports/streamable-http.js +1287 -0
  218. package/dist/core/transports/streamable-http.js.map +1 -0
  219. package/dist/core/types.d.ts +195 -0
  220. package/dist/core/types.d.ts.map +1 -0
  221. package/dist/core/types.js +2 -0
  222. package/dist/core/types.js.map +1 -0
  223. package/dist/core/widgets/widget-examples.resource.d.ts +17 -0
  224. package/dist/core/widgets/widget-examples.resource.d.ts.map +1 -0
  225. package/dist/core/widgets/widget-examples.resource.js +28 -0
  226. package/dist/core/widgets/widget-examples.resource.js.map +1 -0
  227. package/dist/core/widgets/widget-registry.d.ts +56 -0
  228. package/dist/core/widgets/widget-registry.d.ts.map +1 -0
  229. package/dist/core/widgets/widget-registry.js +75 -0
  230. package/dist/core/widgets/widget-registry.js.map +1 -0
  231. package/dist/testing/index.d.ts +103 -0
  232. package/dist/testing/index.d.ts.map +1 -0
  233. package/dist/testing/index.js +161 -0
  234. package/dist/testing/index.js.map +1 -0
  235. package/dist/ui-next/index.d.ts +31 -0
  236. package/dist/ui-next/index.d.ts.map +1 -0
  237. package/dist/ui-next/index.js +687 -0
  238. package/dist/ui-next/index.js.map +1 -0
  239. package/package.json +89 -0
@@ -0,0 +1,1287 @@
1
+ /**
2
+ * Streamable HTTP Transport for MCP
3
+ *
4
+ * Implements the MCP Streamable HTTP transport specification (2025-06-18).
5
+ * https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http
6
+ *
7
+ * Features:
8
+ * - Single MCP endpoint supporting both POST and GET
9
+ * - POST for sending messages to server
10
+ * - GET for SSE streams from server
11
+ * - Session management with Mcp-Session-Id header
12
+ * - Resumability support with Last-Event-ID
13
+ * - Multiple concurrent client connections
14
+ * - Protocol version header support
15
+ */
16
+ import express from 'express';
17
+ import { v4 as uuidv4 } from 'uuid';
18
+ import { readFileSync } from 'fs';
19
+ import { fileURLToPath } from 'url';
20
+ import { dirname, join } from 'path';
21
+ /**
22
+ * Streamable HTTP Transport
23
+ * Implements MCP Streamable HTTP specification
24
+ */
25
+ export class StreamableHttpTransport {
26
+ app;
27
+ server = null;
28
+ sessions = new Map();
29
+ activeStreams = new Map(); // For sessionless mode
30
+ messageHandler;
31
+ closeHandler;
32
+ errorHandler;
33
+ options;
34
+ sessionCleanupInterval;
35
+ getToolsCallback;
36
+ serverConfig;
37
+ logoBase64;
38
+ constructor(options = {}) {
39
+ this.options = {
40
+ port: options.port || 3000,
41
+ host: options.host || 'localhost',
42
+ endpoint: options.endpoint || '/mcp',
43
+ enableSessions: options.enableSessions === true, // Default to false for simpler clients
44
+ sessionTimeout: options.sessionTimeout || 30 * 60 * 1000, // 30 minutes
45
+ enableCors: options.enableCors !== false, // Default to true
46
+ };
47
+ this.app = options.app || express();
48
+ // CRITICAL: Disable Express's automatic OPTIONS handling
49
+ this.app.set('x-powered-by', false);
50
+ // Enable trust proxy to respect X-Forwarded-* headers from reverse proxies
51
+ // This is essential for HTTPS detection when behind a proxy
52
+ this.app.set('trust proxy', true);
53
+ // Load logo for documentation page
54
+ this.loadLogo();
55
+ this.setupMiddleware();
56
+ this.setupRoutes();
57
+ this.startSessionCleanup();
58
+ }
59
+ /**
60
+ * Load logo image as base64 for embedding in documentation page
61
+ */
62
+ loadLogo() {
63
+ try {
64
+ const __filename = fileURLToPath(import.meta.url);
65
+ const __dirname = dirname(__filename);
66
+ // Try multiple paths:
67
+ // 1. From dist/core/transports/streamable-http.js -> ../../../src/assets/nitrocloud.png (package source)
68
+ // 2. From dist/core/transports/streamable-http.js -> ../../../../src/assets/nitrocloud.png (if in nitrostack package)
69
+ // 3. From project root (user's project) -> src/assets/nitrocloud.png
70
+ const possiblePaths = [
71
+ join(__dirname, '../../../src/assets/nitrocloud.png'), // From dist/core/transports -> src/assets
72
+ join(__dirname, '../../../../src/assets/nitrocloud.png'), // From dist/core/transports -> src/assets (alternative)
73
+ join(process.cwd(), 'src/assets/nitrocloud.png'), // User's project
74
+ join(process.cwd(), 'node_modules/nitrostack/src/assets/nitrocloud.png'), // From node_modules
75
+ ];
76
+ let logoPath = null;
77
+ for (const path of possiblePaths) {
78
+ try {
79
+ if (readFileSync(path, { flag: 'r' })) {
80
+ logoPath = path;
81
+ break;
82
+ }
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ }
88
+ if (logoPath) {
89
+ const logoBuffer = readFileSync(logoPath);
90
+ this.logoBase64 = logoBuffer.toString('base64');
91
+ }
92
+ else {
93
+ this.logoBase64 = undefined;
94
+ }
95
+ }
96
+ catch (error) {
97
+ // Logo is optional, continue without it
98
+ this.logoBase64 = undefined;
99
+ }
100
+ }
101
+ /**
102
+ * Setup Express middleware
103
+ */
104
+ setupMiddleware() {
105
+ // CORS (if enabled) - MUST be the very first middleware, handles ALL requests
106
+ if (this.options.enableCors) {
107
+ // Add CORS headers to ALL responses
108
+ this.app.use((req, res, next) => {
109
+ res.setHeader('Access-Control-Allow-Origin', '*');
110
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
111
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID');
112
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
113
+ // Handle OPTIONS immediately
114
+ if (req.method === 'OPTIONS') {
115
+ res.status(200).end();
116
+ return;
117
+ }
118
+ next();
119
+ });
120
+ }
121
+ // Security: Validate Origin header to prevent DNS rebinding attacks (skip if CORS enabled)
122
+ if (!this.options.enableCors) {
123
+ this.app.use((req, res, next) => {
124
+ const origin = req.get('Origin');
125
+ const host = req.get('Host');
126
+ if (origin && host) {
127
+ const originHost = new URL(origin).host;
128
+ if (originHost !== host && !this.isLocalhost(originHost)) {
129
+ res.status(403).json({ error: 'Invalid Origin header' });
130
+ return;
131
+ }
132
+ }
133
+ next();
134
+ });
135
+ }
136
+ // JSON parsing
137
+ this.app.use(express.json());
138
+ }
139
+ /**
140
+ * Setup MCP endpoint routes
141
+ */
142
+ setupRoutes() {
143
+ const endpoint = this.options.endpoint;
144
+ // IMPORTANT: Add OPTIONS handlers FIRST to override Express's auto-OPTIONS
145
+ if (this.options.enableCors) {
146
+ // Main endpoint OPTIONS
147
+ this.app.options(endpoint, (req, res) => {
148
+ res.sendStatus(200);
149
+ });
150
+ // SSE endpoint OPTIONS
151
+ this.app.options(`${endpoint}/sse`, (req, res) => {
152
+ res.sendStatus(200);
153
+ });
154
+ // Message endpoint OPTIONS
155
+ this.app.options(`${endpoint}/message`, (req, res) => {
156
+ res.sendStatus(200);
157
+ });
158
+ }
159
+ // MCP Endpoint - POST for sending messages to server (main endpoint)
160
+ this.app.post(endpoint, async (req, res) => {
161
+ await this.handlePost(req, res);
162
+ });
163
+ // Legacy message endpoint for backward compatibility with old HTTP transport clients
164
+ // Some clients may POST to /mcp/message instead of /mcp
165
+ this.app.post(`${endpoint}/message`, async (req, res) => {
166
+ await this.handlePost(req, res);
167
+ });
168
+ // MCP Endpoint - GET for SSE streams (main endpoint)
169
+ this.app.get(endpoint, (req, res) => {
170
+ this.handleGet(req, res);
171
+ });
172
+ // Legacy SSE endpoint for backward compatibility with old HTTP transport clients
173
+ // Some clients may connect to /mcp/sse instead of /mcp
174
+ this.app.get(`${endpoint}/sse`, (req, res) => {
175
+ this.handleGet(req, res);
176
+ });
177
+ // MCP Endpoint - DELETE for session termination
178
+ this.app.delete(endpoint, (req, res) => {
179
+ this.handleDelete(req, res);
180
+ });
181
+ // Backward compatibility: /sse endpoint (alias for GET /mcp)
182
+ this.app.get(`${endpoint}/sse`, (req, res) => {
183
+ this.handleGet(req, res);
184
+ });
185
+ // Backward compatibility: /message endpoint (alias for POST /mcp)
186
+ this.app.post(`${endpoint}/message`, async (req, res) => {
187
+ // Simple message handler that doesn't require all the session/SSE logic
188
+ try {
189
+ const message = req.body;
190
+ if (!message || !message.jsonrpc) {
191
+ res.status(400).json({ error: 'Invalid JSON-RPC message' });
192
+ return;
193
+ }
194
+ // Pass to message handler
195
+ if (this.messageHandler) {
196
+ await this.messageHandler(message);
197
+ }
198
+ res.json({ status: 'received' });
199
+ }
200
+ catch (error) {
201
+ console.error('Error handling message:', error);
202
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
203
+ }
204
+ });
205
+ // Info endpoint for GET on /message
206
+ this.app.get(`${endpoint}/message`, (req, res) => {
207
+ res.json({
208
+ endpoint: `${endpoint}/message`,
209
+ method: 'POST',
210
+ description: 'Send JSON-RPC messages to the MCP server',
211
+ usage: 'POST with Content-Type: application/json',
212
+ example: {
213
+ jsonrpc: '2.0',
214
+ method: 'initialize',
215
+ params: {
216
+ protocolVersion: '2024-11-05',
217
+ capabilities: {},
218
+ clientInfo: { name: 'test-client', version: '1.0.0' }
219
+ },
220
+ id: 1
221
+ }
222
+ });
223
+ });
224
+ // Health check
225
+ this.app.get(`${endpoint}/health`, (req, res) => {
226
+ res.json({
227
+ status: 'ok',
228
+ transport: 'streamable-http',
229
+ version: '2025-06-18',
230
+ sessions: this.sessions.size,
231
+ uptime: process.uptime(),
232
+ });
233
+ });
234
+ // Root documentation page (only in production mode when HTTP server runs)
235
+ // This route is added at the end to avoid conflicts with MCP endpoints
236
+ if (process.env.NODE_ENV !== 'development') {
237
+ this.app.get('/', async (req, res) => {
238
+ try {
239
+ const tools = this.getToolsCallback ? await this.getToolsCallback() : [];
240
+ // Get host from request headers (supports X-Forwarded-Host for reverse proxies)
241
+ let host = req.get('x-forwarded-host') || req.get('host') || `${this.options.host}:${this.options.port}`;
242
+ // In production, remove port if it's standard HTTP/HTTPS port
243
+ // This handles cases where the server is behind a reverse proxy
244
+ if (process.env.NODE_ENV === 'production') {
245
+ // Remove port if it's 80 (HTTP) or 443 (HTTPS)
246
+ host = host.replace(/:(80|443)$/, '');
247
+ }
248
+ // Support X-Forwarded-Proto for reverse proxies (production deployments)
249
+ const protocol = req.get('x-forwarded-proto') || req.protocol || 'http';
250
+ const baseUrl = `${protocol}://${host}`;
251
+ const mcpEndpoint = `${baseUrl}${endpoint}`;
252
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
253
+ res.send(this.generateDocumentationPage(tools, mcpEndpoint));
254
+ }
255
+ catch (error) {
256
+ console.error('Error generating documentation page:', error);
257
+ res.status(500).send('Error generating documentation page');
258
+ }
259
+ });
260
+ }
261
+ }
262
+ /**
263
+ * Handle POST requests (client sending messages to server)
264
+ */
265
+ async handlePost(req, res) {
266
+ try {
267
+ const message = req.body;
268
+ const sessionId = req.get('Mcp-Session-Id');
269
+ const accept = req.get('Accept') || '';
270
+ // Validate JSON-RPC message
271
+ if (!message || !message.jsonrpc || message.jsonrpc !== '2.0') {
272
+ res.status(400).json({
273
+ jsonrpc: '2.0',
274
+ error: { code: -32600, message: 'Invalid JSON-RPC message' }
275
+ });
276
+ return;
277
+ }
278
+ // Check session
279
+ if (this.options.enableSessions && sessionId) {
280
+ const session = this.sessions.get(sessionId);
281
+ if (!session) {
282
+ res.status(404).json({
283
+ jsonrpc: '2.0',
284
+ error: { code: -32001, message: 'Session not found' }
285
+ });
286
+ return;
287
+ }
288
+ session.lastActivity = Date.now();
289
+ }
290
+ // Handle different message types
291
+ const messageType = this.getMessageType(message);
292
+ if (messageType === 'notification' || messageType === 'response') {
293
+ // Notification or Response: Return 202 Accepted
294
+ if (this.messageHandler) {
295
+ await this.messageHandler(message);
296
+ }
297
+ res.status(202).send();
298
+ return;
299
+ }
300
+ if (messageType === 'request') {
301
+ // Request: Accept header check (be lenient - if not specified, assume they want SSE)
302
+ const supportsSSE = !accept || accept.includes('text/event-stream') || accept.includes('*/*');
303
+ const supportsJSON = accept.includes('application/json');
304
+ // Pass to message handler
305
+ if (this.messageHandler) {
306
+ await this.messageHandler(message);
307
+ }
308
+ // For InitializeRequest, create session if enabled
309
+ if (this.isInitializeRequest(message)) {
310
+ if (this.options.enableSessions && !sessionId) {
311
+ const newSessionId = this.generateSessionId();
312
+ const session = {
313
+ id: newSessionId,
314
+ streams: new Map(),
315
+ lastActivity: Date.now(),
316
+ messageQueue: [],
317
+ eventIdCounter: 0,
318
+ };
319
+ this.sessions.set(newSessionId, session);
320
+ res.setHeader('Mcp-Session-Id', newSessionId);
321
+ }
322
+ }
323
+ // For SSE: Just acknowledge receipt, response will come via existing SSE stream
324
+ if (supportsSSE) {
325
+ // Accept the request
326
+ res.status(202).send();
327
+ // Response will be sent via the send() method to existing SSE streams
328
+ }
329
+ else {
330
+ // Single JSON response (less common)
331
+ res.setHeader('Content-Type', 'application/json');
332
+ // Response will be sent by the protocol layer
333
+ res._mcpWaitingForResponse = true;
334
+ res._mcpRequestId = message.id;
335
+ }
336
+ }
337
+ }
338
+ catch (error) {
339
+ console.error('POST error:', error);
340
+ res.status(500).json({
341
+ jsonrpc: '2.0',
342
+ error: { code: -32603, message: 'Internal error' }
343
+ });
344
+ }
345
+ }
346
+ /**
347
+ * Handle GET requests (client opening SSE stream)
348
+ */
349
+ handleGet(req, res) {
350
+ const sessionId = req.get('Mcp-Session-Id');
351
+ const lastEventId = req.get('Last-Event-ID');
352
+ const accept = req.get('Accept') || '';
353
+ // Check if client explicitly doesn't want SSE (e.g., asking for JSON only)
354
+ const rejectsSSE = accept && !accept.includes('*/*') && !accept.includes('text/event-stream') && accept.length > 0;
355
+ if (rejectsSSE) {
356
+ res.status(405).send('Method Not Allowed - This endpoint provides Server-Sent Events');
357
+ return;
358
+ }
359
+ // Check session
360
+ let session;
361
+ if (this.options.enableSessions) {
362
+ if (!sessionId) {
363
+ res.status(400).json({ error: 'Mcp-Session-Id required' });
364
+ return;
365
+ }
366
+ session = this.sessions.get(sessionId);
367
+ if (!session) {
368
+ res.status(404).json({ error: 'Session not found' });
369
+ return;
370
+ }
371
+ session.lastActivity = Date.now();
372
+ }
373
+ // Setup SSE
374
+ // CRITICAL: Set CORS headers for SSE if enabled (must be before flushHeaders)
375
+ // SSE connections need CORS headers explicitly set before the stream starts
376
+ if (this.options.enableCors) {
377
+ res.setHeader('Access-Control-Allow-Origin', '*');
378
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
379
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID');
380
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
381
+ }
382
+ res.setHeader('Content-Type', 'text/event-stream');
383
+ res.setHeader('Cache-Control', 'no-cache');
384
+ res.setHeader('Connection', 'keep-alive');
385
+ res.flushHeaders();
386
+ // Create stream
387
+ const streamId = uuidv4();
388
+ const stream = {
389
+ id: streamId,
390
+ response: res,
391
+ eventIdCounter: 0,
392
+ closed: false,
393
+ };
394
+ // Send endpoint event immediately (required by MCP SDK)
395
+ // This tells the client where to POST messages
396
+ // Support X-Forwarded-Proto for reverse proxies (production deployments)
397
+ // This ensures the endpoint URL matches the client's connection protocol (HTTPS)
398
+ const protocol = req.get('x-forwarded-proto') || req.protocol || 'http';
399
+ // Detect client format based on request URL:
400
+ // - If client connects to /mcp/sse (old format) → return /mcp/message as endpoint
401
+ // - If client connects to /mcp (new format) → return /mcp as endpoint
402
+ // Use originalUrl or url to get the full path including /sse
403
+ const requestPath = req.originalUrl || req.url || req.path;
404
+ const isOldFormat = requestPath.includes(`${this.options.endpoint}/sse`) ||
405
+ requestPath.endsWith('/sse') ||
406
+ req.path === `${this.options.endpoint}/sse`;
407
+ const messageEndpoint = isOldFormat
408
+ ? `${this.options.endpoint}/message` // Old format: POST to /mcp/message
409
+ : this.options.endpoint; // New format: POST to /mcp
410
+ const endpointUrl = `${protocol}://${req.get('host')}${messageEndpoint}`;
411
+ try {
412
+ res.write(`event: endpoint\n`);
413
+ res.write(`data: ${endpointUrl}\n\n`);
414
+ }
415
+ catch (error) {
416
+ console.error('Error sending endpoint event:', error);
417
+ stream.closed = true;
418
+ return;
419
+ }
420
+ // Add to session or activeStreams
421
+ if (session) {
422
+ session.streams.set(streamId, stream);
423
+ // Resume support: replay messages after lastEventId
424
+ if (lastEventId) {
425
+ this.replayMessages(session, stream, lastEventId);
426
+ }
427
+ }
428
+ else {
429
+ // Sessionless mode: track in activeStreams
430
+ this.activeStreams.set(streamId, stream);
431
+ }
432
+ // Handle client disconnect
433
+ req.on('close', () => {
434
+ stream.closed = true;
435
+ if (session) {
436
+ session.streams.delete(streamId);
437
+ }
438
+ else {
439
+ this.activeStreams.delete(streamId);
440
+ }
441
+ });
442
+ // Send ping every 30 seconds to keep connection alive
443
+ const pingInterval = setInterval(() => {
444
+ if (stream.closed) {
445
+ clearInterval(pingInterval);
446
+ return;
447
+ }
448
+ try {
449
+ res.write(': ping\n\n');
450
+ }
451
+ catch (error) {
452
+ clearInterval(pingInterval);
453
+ stream.closed = true;
454
+ }
455
+ }, 30000);
456
+ }
457
+ /**
458
+ * Handle DELETE requests (session termination)
459
+ */
460
+ handleDelete(req, res) {
461
+ const sessionId = req.get('Mcp-Session-Id');
462
+ if (!sessionId) {
463
+ res.status(400).json({ error: 'Mcp-Session-Id required' });
464
+ return;
465
+ }
466
+ const session = this.sessions.get(sessionId);
467
+ if (!session) {
468
+ res.status(404).json({ error: 'Session not found' });
469
+ return;
470
+ }
471
+ // Close all streams
472
+ for (const stream of session.streams.values()) {
473
+ try {
474
+ stream.response.end();
475
+ stream.closed = true;
476
+ }
477
+ catch (error) {
478
+ // Ignore
479
+ }
480
+ }
481
+ // Remove session
482
+ this.sessions.delete(sessionId);
483
+ res.status(200).json({ status: 'session terminated' });
484
+ }
485
+ /**
486
+ * Start SSE stream for a request
487
+ */
488
+ async startSSEStream(req, res, request, sessionId) {
489
+ // Setup SSE
490
+ res.setHeader('Content-Type', 'text/event-stream');
491
+ res.setHeader('Cache-Control', 'no-cache');
492
+ res.setHeader('Connection', 'keep-alive');
493
+ res.flushHeaders();
494
+ // Create stream
495
+ const streamId = uuidv4();
496
+ const stream = {
497
+ id: streamId,
498
+ response: res,
499
+ eventIdCounter: 0,
500
+ closed: false,
501
+ };
502
+ // Store stream reference for this request
503
+ req._mcpStreamId = streamId;
504
+ req._mcpStream = stream;
505
+ // Add to session or activeStreams
506
+ if (sessionId) {
507
+ const session = this.sessions.get(sessionId);
508
+ if (session) {
509
+ session.streams.set(streamId, stream);
510
+ }
511
+ }
512
+ else {
513
+ // Sessionless mode: track in activeStreams
514
+ this.activeStreams.set(streamId, stream);
515
+ }
516
+ // Handle client disconnect
517
+ req.on('close', () => {
518
+ stream.closed = true;
519
+ if (sessionId) {
520
+ const session = this.sessions.get(sessionId);
521
+ if (session) {
522
+ session.streams.delete(streamId);
523
+ }
524
+ }
525
+ else {
526
+ this.activeStreams.delete(streamId);
527
+ }
528
+ });
529
+ }
530
+ /**
531
+ * Send message to client(s)
532
+ */
533
+ async send(message) {
534
+ // Find target session/stream
535
+ // For responses, send to the stream that made the request
536
+ if (this.isResponse(message)) {
537
+ const response = message;
538
+ await this.sendToRequestStream(response);
539
+ return;
540
+ }
541
+ // For requests and notifications, send to all active streams
542
+ // First, send to session-based streams
543
+ for (const session of this.sessions.values()) {
544
+ for (const stream of session.streams.values()) {
545
+ if (!stream.closed) {
546
+ await this.sendToStream(stream, message, session);
547
+ }
548
+ }
549
+ }
550
+ // Then, send to sessionless streams
551
+ for (const stream of this.activeStreams.values()) {
552
+ if (!stream.closed) {
553
+ await this.sendToStreamSessionless(stream, message);
554
+ }
555
+ }
556
+ }
557
+ /**
558
+ * Send message to a specific stream
559
+ */
560
+ async sendToStream(stream, message, session) {
561
+ try {
562
+ const eventId = `${session.id}-${++stream.eventIdCounter}`;
563
+ const data = JSON.stringify(message);
564
+ stream.response.write(`id: ${eventId}\n`);
565
+ stream.response.write(`data: ${data}\n\n`);
566
+ // Store in queue for resumability
567
+ session.messageQueue.push({
568
+ message,
569
+ streamId: stream.id,
570
+ eventId,
571
+ });
572
+ }
573
+ catch (error) {
574
+ console.error('Error sending to stream:', error);
575
+ stream.closed = true;
576
+ }
577
+ }
578
+ /**
579
+ * Send response to the stream that made the request
580
+ */
581
+ async sendToRequestStream(response) {
582
+ // Find the stream associated with this request
583
+ // For session-based streams
584
+ for (const session of this.sessions.values()) {
585
+ for (const stream of session.streams.values()) {
586
+ if (!stream.closed) {
587
+ await this.sendToStream(stream, response, session);
588
+ // Keep stream open for multiple requests - stream will close when client disconnects
589
+ }
590
+ }
591
+ }
592
+ // For sessionless streams - CRITICAL: Keep stream open for multiple requests
593
+ // The SSE stream should stay open to handle initialize, ping, tool calls, etc.
594
+ // Closing it after the first response breaks subsequent requests
595
+ for (const stream of this.activeStreams.values()) {
596
+ if (!stream.closed) {
597
+ await this.sendToStreamSessionless(stream, response);
598
+ // Keep stream open - it will be closed when the client disconnects naturally
599
+ }
600
+ }
601
+ }
602
+ /**
603
+ * Send message to a sessionless stream
604
+ */
605
+ async sendToStreamSessionless(stream, message) {
606
+ try {
607
+ const eventId = `${stream.id}-${++stream.eventIdCounter}`;
608
+ const data = JSON.stringify(message);
609
+ stream.response.write(`id: ${eventId}\n`);
610
+ stream.response.write(`data: ${data}\n\n`);
611
+ }
612
+ catch (error) {
613
+ console.error('Error sending to sessionless stream:', error);
614
+ stream.closed = true;
615
+ this.activeStreams.delete(stream.id);
616
+ }
617
+ }
618
+ /**
619
+ * Replay messages for resumability
620
+ */
621
+ replayMessages(session, stream, lastEventId) {
622
+ const messages = session.messageQueue.filter((msg) => msg.streamId === stream.id && msg.eventId > lastEventId);
623
+ for (const { message, eventId } of messages) {
624
+ try {
625
+ const data = JSON.stringify(message);
626
+ stream.response.write(`id: ${eventId}\n`);
627
+ stream.response.write(`data: ${data}\n\n`);
628
+ }
629
+ catch (error) {
630
+ console.error('Error replaying message:', error);
631
+ break;
632
+ }
633
+ }
634
+ }
635
+ /**
636
+ * Start the HTTP server
637
+ */
638
+ async start() {
639
+ if (this.server) {
640
+ await this.close();
641
+ }
642
+ return new Promise((resolve, reject) => {
643
+ const errorHandler = (error) => {
644
+ console.error(`Failed to start Streamable HTTP transport: ${error.message}`);
645
+ this.server = null;
646
+ reject(error);
647
+ };
648
+ try {
649
+ const server = this.app.listen(this.options.port, this.options.host);
650
+ server.once('error', errorHandler);
651
+ server.once('listening', () => {
652
+ server.removeListener('error', errorHandler);
653
+ server.on('error', (error) => {
654
+ if (this.errorHandler) {
655
+ this.errorHandler(error);
656
+ }
657
+ });
658
+ this.server = server;
659
+ console.error(`🌐 MCP Streamable HTTP transport listening on http://${this.options.host}:${this.options.port}${this.options.endpoint}`);
660
+ console.error(` Protocol: MCP 2025-06-18`);
661
+ console.error(` Sessions: ${this.options.enableSessions ? 'enabled' : 'disabled'}`);
662
+ resolve();
663
+ });
664
+ }
665
+ catch (error) {
666
+ reject(error);
667
+ }
668
+ });
669
+ }
670
+ /**
671
+ * Register additional HTTP routes
672
+ * Allows modules (like OAuthModule) to add custom endpoints
673
+ */
674
+ on(path, handler) {
675
+ this.app.get(path, handler);
676
+ }
677
+ /**
678
+ * Close the transport
679
+ */
680
+ async close() {
681
+ // Clear session cleanup
682
+ if (this.sessionCleanupInterval) {
683
+ clearInterval(this.sessionCleanupInterval);
684
+ }
685
+ // Close all sessions
686
+ for (const session of this.sessions.values()) {
687
+ for (const stream of session.streams.values()) {
688
+ try {
689
+ stream.response.end();
690
+ stream.closed = true;
691
+ }
692
+ catch (error) {
693
+ // Ignore
694
+ }
695
+ }
696
+ }
697
+ this.sessions.clear();
698
+ // Close HTTP server
699
+ if (this.server) {
700
+ return new Promise((resolve) => {
701
+ const server = this.server;
702
+ this.server = null;
703
+ server.closeAllConnections?.();
704
+ server.close((err) => {
705
+ if (err) {
706
+ console.error('HTTP server close error:', err.message);
707
+ }
708
+ if (this.closeHandler) {
709
+ this.closeHandler();
710
+ }
711
+ resolve();
712
+ });
713
+ });
714
+ }
715
+ if (this.closeHandler) {
716
+ this.closeHandler();
717
+ }
718
+ }
719
+ /**
720
+ * Set message handler
721
+ */
722
+ set onmessage(handler) {
723
+ this.messageHandler = handler;
724
+ }
725
+ /**
726
+ * Set close handler
727
+ */
728
+ set onclose(handler) {
729
+ this.closeHandler = handler;
730
+ }
731
+ /**
732
+ * Set error handler
733
+ */
734
+ set onerror(handler) {
735
+ this.errorHandler = handler;
736
+ }
737
+ /**
738
+ * Start session cleanup interval
739
+ */
740
+ startSessionCleanup() {
741
+ this.sessionCleanupInterval = setInterval(() => {
742
+ const now = Date.now();
743
+ for (const [sessionId, session] of this.sessions.entries()) {
744
+ if (now - session.lastActivity > this.options.sessionTimeout) {
745
+ // Cleanup expired session
746
+ for (const stream of session.streams.values()) {
747
+ try {
748
+ stream.response.end();
749
+ stream.closed = true;
750
+ }
751
+ catch (error) {
752
+ // Ignore
753
+ }
754
+ }
755
+ this.sessions.delete(sessionId);
756
+ console.error(`Session ${sessionId} expired and cleaned up`);
757
+ }
758
+ }
759
+ }, 60000); // Check every minute
760
+ }
761
+ /**
762
+ * Helper methods
763
+ */
764
+ generateSessionId() {
765
+ return uuidv4();
766
+ }
767
+ getMessageType(message) {
768
+ if ('method' in message && 'id' in message)
769
+ return 'request';
770
+ if ('result' in message || 'error' in message)
771
+ return 'response';
772
+ return 'notification';
773
+ }
774
+ isResponse(message) {
775
+ return 'result' in message || 'error' in message;
776
+ }
777
+ isInitializeRequest(message) {
778
+ return 'method' in message && message.method === 'initialize';
779
+ }
780
+ isLocalhost(host) {
781
+ // Extract hostname without port (handles both IPv4 and IPv6 formats)
782
+ let hostname = host;
783
+ if (host.includes('[') && host.includes(']')) {
784
+ // IPv6 with port format: [::1]:3000
785
+ hostname = host.substring(host.indexOf('[') + 1, host.indexOf(']'));
786
+ }
787
+ else if (host.includes(':') && (host.match(/:/g) || []).length > 1) {
788
+ // Raw IPv6: ::1
789
+ hostname = host;
790
+ }
791
+ else {
792
+ // IPv4 or hostname: localhost:3000 or 127.0.0.1:3000
793
+ hostname = host.split(':')[0];
794
+ }
795
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
796
+ }
797
+ /**
798
+ * Get the Express app (for adding custom routes)
799
+ */
800
+ getApp() {
801
+ return this.app;
802
+ }
803
+ /**
804
+ * Set callback to get tools list for documentation page
805
+ */
806
+ setToolsCallback(callback) {
807
+ this.getToolsCallback = callback;
808
+ }
809
+ /**
810
+ * Set server configuration for documentation page
811
+ */
812
+ setServerConfig(config) {
813
+ this.serverConfig = config;
814
+ }
815
+ /**
816
+ * Generate HTML documentation page
817
+ */
818
+ generateDocumentationPage(tools, mcpEndpoint) {
819
+ const serverName = this.serverConfig?.name || 'NitroStack MCP Server';
820
+ const serverVersion = this.serverConfig?.version || '1.0.0';
821
+ const serverDescription = this.serverConfig?.description || 'A powerful MCP server built with NitroStack';
822
+ return `<!DOCTYPE html>
823
+ <html lang="en">
824
+ <head>
825
+ <meta charset="UTF-8">
826
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
827
+ <title>${serverName} - MCP Server Documentation</title>
828
+ <link rel="preconnect" href="https://fonts.googleapis.com">
829
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
830
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
831
+ <style>
832
+ :root {
833
+ --nitrocloud-primary: hsl(217, 91%, 60%);
834
+ --nitrocloud-primary-dark: hsl(217, 91%, 50%);
835
+ --nitrocloud-gradient-start: hsl(217, 91%, 60%);
836
+ --nitrocloud-gradient-end: hsl(221, 83%, 53%);
837
+ --background: hsl(0, 0%, 100%);
838
+ --foreground: hsl(222.2, 84%, 4.9%);
839
+ --primary: hsl(221.2, 83.2%, 53.3%);
840
+ --primary-foreground: hsl(210, 40%, 98%);
841
+ --secondary: hsl(210, 40%, 96.1%);
842
+ --muted: hsl(210, 40%, 96.1%);
843
+ --muted-foreground: hsl(215.4, 16.3%, 46.9%);
844
+ --border: hsl(214.3, 31.8%, 91.4%);
845
+ --radius: 0.75rem;
846
+ }
847
+ * {
848
+ margin: 0;
849
+ padding: 0;
850
+ box-sizing: border-box;
851
+ }
852
+
853
+ body {
854
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
855
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
856
+ min-height: 100vh;
857
+ padding: 2rem;
858
+ color: var(--foreground);
859
+ line-height: 1.6;
860
+ -webkit-font-smoothing: antialiased;
861
+ -moz-osx-font-smoothing: grayscale;
862
+ }
863
+
864
+ .container {
865
+ max-width: 1280px;
866
+ margin: 0 auto;
867
+ background: var(--background);
868
+ border-radius: 24px;
869
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
870
+ overflow: hidden;
871
+ }
872
+
873
+ .header {
874
+ background: linear-gradient(135deg, var(--nitrocloud-gradient-start) 0%, var(--nitrocloud-gradient-end) 100%);
875
+ color: white;
876
+ padding: 4rem 2rem;
877
+ text-align: center;
878
+ position: relative;
879
+ overflow: hidden;
880
+ }
881
+
882
+ .header::before {
883
+ content: '';
884
+ position: absolute;
885
+ top: 0;
886
+ left: 0;
887
+ right: 0;
888
+ bottom: 0;
889
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 100%);
890
+ pointer-events: none;
891
+ }
892
+
893
+ .header > * {
894
+ position: relative;
895
+ z-index: 1;
896
+ }
897
+
898
+ .logo-container {
899
+ margin-bottom: 2rem;
900
+ display: flex;
901
+ justify-content: center;
902
+ align-items: center;
903
+ }
904
+
905
+ .logo {
906
+ height: 80px;
907
+ width: auto;
908
+ max-width: 200px;
909
+ object-fit: contain;
910
+ filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.3));
911
+ transition: transform 0.3s ease;
912
+ }
913
+
914
+ .logo:hover {
915
+ transform: scale(1.05);
916
+ }
917
+
918
+ .header h1 {
919
+ font-size: 3rem;
920
+ font-weight: 700;
921
+ margin-bottom: 0.5rem;
922
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
923
+ letter-spacing: -0.025em;
924
+ }
925
+
926
+ .header .version {
927
+ font-size: 1rem;
928
+ opacity: 0.95;
929
+ font-weight: 400;
930
+ letter-spacing: 0.05em;
931
+ text-transform: uppercase;
932
+ }
933
+
934
+ .header .description {
935
+ margin-top: 1rem;
936
+ font-size: 1.125rem;
937
+ opacity: 0.95;
938
+ font-weight: 400;
939
+ max-width: 600px;
940
+ margin-left: auto;
941
+ margin-right: auto;
942
+ }
943
+
944
+ .content {
945
+ padding: 3rem 2rem;
946
+ }
947
+
948
+ .section {
949
+ margin-bottom: 4rem;
950
+ }
951
+
952
+ .section:last-child {
953
+ margin-bottom: 0;
954
+ }
955
+
956
+ .section h2 {
957
+ font-size: 2rem;
958
+ font-weight: 700;
959
+ color: var(--foreground);
960
+ margin-bottom: 1.5rem;
961
+ padding-bottom: 0.75rem;
962
+ border-bottom: 3px solid var(--nitrocloud-primary);
963
+ letter-spacing: -0.02em;
964
+ }
965
+
966
+ .connection-info {
967
+ background: linear-gradient(to right, var(--secondary) 0%, var(--muted) 100%);
968
+ border-left: 4px solid var(--nitrocloud-primary);
969
+ padding: 2rem;
970
+ border-radius: var(--radius);
971
+ margin-bottom: 2rem;
972
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
973
+ }
974
+
975
+ .connection-info p {
976
+ font-weight: 600;
977
+ color: var(--foreground);
978
+ margin-bottom: 0.75rem;
979
+ font-size: 0.9375rem;
980
+ }
981
+
982
+ .connection-info code {
983
+ background: hsl(222.2, 84%, 4.9%);
984
+ color: hsl(142, 76%, 36%);
985
+ padding: 1rem 1.25rem;
986
+ border-radius: 8px;
987
+ font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
988
+ display: block;
989
+ margin-top: 0.75rem;
990
+ word-break: break-all;
991
+ font-size: 0.875rem;
992
+ line-height: 1.6;
993
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
994
+ }
995
+
996
+ .connection-info .description {
997
+ margin-top: 1rem;
998
+ color: var(--muted-foreground);
999
+ font-size: 0.9375rem;
1000
+ line-height: 1.6;
1001
+ }
1002
+
1003
+ .tools-grid {
1004
+ display: grid;
1005
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
1006
+ gap: 1.5rem;
1007
+ margin-top: 1.5rem;
1008
+ }
1009
+
1010
+ .tool-card {
1011
+ background: var(--background);
1012
+ border: 2px solid var(--border);
1013
+ border-radius: var(--radius);
1014
+ padding: 1.75rem;
1015
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1016
+ position: relative;
1017
+ overflow: hidden;
1018
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
1019
+ }
1020
+
1021
+ .tool-card:hover {
1022
+ border-color: var(--nitrocloud-primary);
1023
+ box-shadow: 0 8px 24px rgba(59, 159, 255, 0.15);
1024
+ transform: translateY(-4px);
1025
+ }
1026
+
1027
+ .tool-card::before {
1028
+ content: '';
1029
+ position: absolute;
1030
+ top: 0;
1031
+ left: 0;
1032
+ right: 0;
1033
+ height: 4px;
1034
+ background: linear-gradient(90deg, var(--nitrocloud-gradient-start), var(--nitrocloud-gradient-end));
1035
+ }
1036
+
1037
+ .tool-name {
1038
+ font-size: 1.25rem;
1039
+ font-weight: 600;
1040
+ color: var(--foreground);
1041
+ margin-bottom: 0.75rem;
1042
+ display: flex;
1043
+ align-items: center;
1044
+ gap: 0.5rem;
1045
+ letter-spacing: -0.01em;
1046
+ }
1047
+
1048
+ .tool-name::before {
1049
+ content: '⚡';
1050
+ font-size: 1.25rem;
1051
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
1052
+ }
1053
+
1054
+ .tool-description {
1055
+ color: var(--muted-foreground);
1056
+ margin-bottom: 1rem;
1057
+ line-height: 1.625;
1058
+ font-size: 0.9375rem;
1059
+ }
1060
+
1061
+ .tool-schema {
1062
+ background: var(--secondary);
1063
+ border-radius: 8px;
1064
+ padding: 1rem;
1065
+ margin-top: 1rem;
1066
+ font-size: 0.875rem;
1067
+ border: 1px solid var(--border);
1068
+ }
1069
+
1070
+ .tool-schema summary {
1071
+ cursor: pointer;
1072
+ font-weight: 600;
1073
+ color: var(--nitrocloud-primary);
1074
+ margin-bottom: 0.5rem;
1075
+ user-select: none;
1076
+ transition: color 0.2s;
1077
+ }
1078
+
1079
+ .tool-schema summary:hover {
1080
+ color: var(--nitrocloud-primary-dark);
1081
+ }
1082
+
1083
+ .tool-schema pre {
1084
+ background: hsl(222.2, 84%, 4.9%);
1085
+ color: hsl(142, 76%, 36%);
1086
+ padding: 1rem;
1087
+ border-radius: 6px;
1088
+ overflow-x: auto;
1089
+ margin-top: 0.75rem;
1090
+ font-size: 0.8125rem;
1091
+ line-height: 1.6;
1092
+ font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
1093
+ }
1094
+
1095
+ .badge {
1096
+ display: inline-flex;
1097
+ align-items: center;
1098
+ padding: 0.375rem 0.75rem;
1099
+ border-radius: 12px;
1100
+ font-size: 0.75rem;
1101
+ font-weight: 600;
1102
+ margin-top: 0.5rem;
1103
+ transition: all 0.2s;
1104
+ }
1105
+
1106
+ .badge.widget {
1107
+ background: linear-gradient(135deg, hsl(271, 81%, 56%) 0%, hsl(271, 81%, 46%) 100%);
1108
+ color: white;
1109
+ box-shadow: 0 2px 8px rgba(196, 132, 252, 0.3);
1110
+ }
1111
+
1112
+ .empty-state {
1113
+ text-align: center;
1114
+ padding: 4rem 2rem;
1115
+ color: var(--muted-foreground);
1116
+ }
1117
+
1118
+ .empty-state svg {
1119
+ width: 64px;
1120
+ height: 64px;
1121
+ margin: 0 auto 1.5rem;
1122
+ opacity: 0.5;
1123
+ color: var(--muted-foreground);
1124
+ }
1125
+
1126
+ .empty-state p {
1127
+ font-size: 1rem;
1128
+ font-weight: 500;
1129
+ }
1130
+
1131
+ .footer {
1132
+ background: linear-gradient(to right, var(--secondary) 0%, var(--muted) 100%);
1133
+ padding: 2.5rem 2rem;
1134
+ text-align: center;
1135
+ color: var(--muted-foreground);
1136
+ border-top: 1px solid var(--border);
1137
+ }
1138
+
1139
+ .footer p {
1140
+ font-size: 0.9375rem;
1141
+ line-height: 1.6;
1142
+ }
1143
+
1144
+ .footer a {
1145
+ color: var(--nitrocloud-primary);
1146
+ text-decoration: none;
1147
+ font-weight: 600;
1148
+ transition: color 0.2s;
1149
+ }
1150
+
1151
+ .footer a:hover {
1152
+ color: var(--nitrocloud-primary-dark);
1153
+ text-decoration: underline;
1154
+ }
1155
+
1156
+ @media (max-width: 768px) {
1157
+ body {
1158
+ padding: 1rem;
1159
+ }
1160
+
1161
+ .header {
1162
+ padding: 3rem 1.5rem;
1163
+ }
1164
+
1165
+ .header h1 {
1166
+ font-size: 2.25rem;
1167
+ }
1168
+
1169
+ .content {
1170
+ padding: 2rem 1.5rem;
1171
+ }
1172
+
1173
+ .section h2 {
1174
+ font-size: 1.75rem;
1175
+ }
1176
+
1177
+ .tools-grid {
1178
+ grid-template-columns: 1fr;
1179
+ }
1180
+
1181
+ .connection-info {
1182
+ padding: 1.5rem;
1183
+ }
1184
+ }
1185
+
1186
+ @media (prefers-color-scheme: dark) {
1187
+ :root {
1188
+ --background: hsl(222.2, 84%, 4.9%);
1189
+ --foreground: hsl(210, 40%, 98%);
1190
+ --primary: hsl(217, 91%, 60%);
1191
+ --secondary: hsl(217.2, 32.6%, 17.5%);
1192
+ --muted: hsl(217.2, 32.6%, 17.5%);
1193
+ --muted-foreground: hsl(215, 20.2%, 65.1%);
1194
+ --border: hsl(217.2, 32.6%, 17.5%);
1195
+ }
1196
+
1197
+ .connection-info code {
1198
+ background: hsl(217.2, 32.6%, 17.5%);
1199
+ color: hsl(142, 76%, 56%);
1200
+ }
1201
+
1202
+ .tool-schema pre {
1203
+ background: hsl(217.2, 32.6%, 17.5%);
1204
+ color: hsl(142, 76%, 56%);
1205
+ }
1206
+ }
1207
+ </style>
1208
+ </head>
1209
+ <body>
1210
+ <div class="container">
1211
+ <div class="header">
1212
+ ${this.logoBase64 ? `
1213
+ <div class="logo-container">
1214
+ <img src="data:image/png;base64,${this.logoBase64}" alt="NitroCloud Logo" class="logo">
1215
+ </div>
1216
+ ` : ''}
1217
+ <h1>${serverName}</h1>
1218
+ <div class="version">v${serverVersion}</div>
1219
+ <div class="description">${serverDescription}</div>
1220
+ </div>
1221
+
1222
+ <div class="content">
1223
+ <div class="section">
1224
+ <h2>🔌 Connection Information</h2>
1225
+ <div class="connection-info">
1226
+ <p>MCP Endpoint</p>
1227
+ <code>${mcpEndpoint}</code>
1228
+ <p class="description">
1229
+ Connect to this MCP server using the endpoint above. The server supports Server-Sent Events (SSE) for real-time bidirectional communication following the Model Context Protocol specification.
1230
+ </p>
1231
+ </div>
1232
+ </div>
1233
+
1234
+ <div class="section">
1235
+ <h2>🛠️ Available Tools</h2>
1236
+ ${tools.length > 0 ? `
1237
+ <div class="tools-grid">
1238
+ ${tools.map(tool => `
1239
+ <div class="tool-card">
1240
+ <div class="tool-name">${this.escapeHtml(tool.name)}</div>
1241
+ <div class="tool-description">${this.escapeHtml(tool.description || 'No description available')}</div>
1242
+ ${tool.widget || tool.outputTemplate || tool._meta?.['openai/outputTemplate'] ? `
1243
+ <span class="badge widget">🎨 Has UI Widget</span>
1244
+ ` : ''}
1245
+ ${tool.inputSchema ? `
1246
+ <details class="tool-schema">
1247
+ <summary>Input Schema</summary>
1248
+ <pre>${this.escapeHtml(JSON.stringify(tool.inputSchema, null, 2))}</pre>
1249
+ </details>
1250
+ ` : ''}
1251
+ </div>
1252
+ `).join('')}
1253
+ </div>
1254
+ ` : `
1255
+ <div class="empty-state">
1256
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
1257
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
1258
+ </svg>
1259
+ <p>No tools are currently registered on this server.</p>
1260
+ </div>
1261
+ `}
1262
+ </div>
1263
+ </div>
1264
+
1265
+ <div class="footer">
1266
+ <p>Built with <a href="https://nitrostack.ai" target="_blank" rel="noopener noreferrer">NitroStack</a> - The TypeScript MCP Framework</p>
1267
+ <p style="margin-top: 0.5rem; font-size: 0.875rem;">Model Context Protocol Server</p>
1268
+ </div>
1269
+ </div>
1270
+ </body>
1271
+ </html>`;
1272
+ }
1273
+ /**
1274
+ * Escape HTML to prevent XSS
1275
+ */
1276
+ escapeHtml(text) {
1277
+ const map = {
1278
+ '&': '&amp;',
1279
+ '<': '&lt;',
1280
+ '>': '&gt;',
1281
+ '"': '&quot;',
1282
+ "'": '&#039;',
1283
+ };
1284
+ return text.replace(/[&<>"']/g, (m) => map[m]);
1285
+ }
1286
+ }
1287
+ //# sourceMappingURL=streamable-http.js.map