mcp4openapi 0.1.0 → 0.2.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 (151) hide show
  1. package/README.md +134 -95
  2. package/dist/scripts/validate-profile.js +3 -3
  3. package/dist/scripts/validate-profile.js.map +1 -1
  4. package/dist/src/composite-executor.d.ts +3 -1
  5. package/dist/src/composite-executor.d.ts.map +1 -1
  6. package/dist/src/composite-executor.js +16 -5
  7. package/dist/src/composite-executor.js.map +1 -1
  8. package/dist/src/constants.d.ts +49 -0
  9. package/dist/src/constants.d.ts.map +1 -1
  10. package/dist/src/constants.js +49 -0
  11. package/dist/src/constants.js.map +1 -1
  12. package/dist/src/errors.d.ts +6 -0
  13. package/dist/src/errors.d.ts.map +1 -1
  14. package/dist/src/errors.js +13 -0
  15. package/dist/src/errors.js.map +1 -1
  16. package/dist/src/generated-schemas.d.ts +832 -52
  17. package/dist/src/generated-schemas.d.ts.map +1 -1
  18. package/dist/src/generated-schemas.js +31 -8
  19. package/dist/src/generated-schemas.js.map +1 -1
  20. package/dist/src/http-client-factory.d.ts.map +1 -1
  21. package/dist/src/http-client-factory.js +14 -3
  22. package/dist/src/http-client-factory.js.map +1 -1
  23. package/dist/src/http-transport.d.ts +65 -0
  24. package/dist/src/http-transport.d.ts.map +1 -1
  25. package/dist/src/http-transport.js +921 -77
  26. package/dist/src/http-transport.js.map +1 -1
  27. package/dist/src/index.js +108 -8
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/interceptors.d.ts +3 -0
  30. package/dist/src/interceptors.d.ts.map +1 -1
  31. package/dist/src/interceptors.js +50 -8
  32. package/dist/src/interceptors.js.map +1 -1
  33. package/dist/src/logger.d.ts +1 -1
  34. package/dist/src/logger.js +3 -3
  35. package/dist/src/logger.js.map +1 -1
  36. package/dist/src/mcp-server.d.ts +33 -0
  37. package/dist/src/mcp-server.d.ts.map +1 -1
  38. package/dist/src/mcp-server.js +263 -54
  39. package/dist/src/mcp-server.js.map +1 -1
  40. package/dist/src/oauth-provider.d.ts +92 -0
  41. package/dist/src/oauth-provider.d.ts.map +1 -0
  42. package/dist/src/oauth-provider.js +588 -0
  43. package/dist/src/oauth-provider.js.map +1 -0
  44. package/dist/src/openapi-parser.d.ts +16 -0
  45. package/dist/src/openapi-parser.d.ts.map +1 -1
  46. package/dist/src/openapi-parser.js +141 -6
  47. package/dist/src/openapi-parser.js.map +1 -1
  48. package/dist/src/profile-loader.d.ts +2 -2
  49. package/dist/src/profile-loader.d.ts.map +1 -1
  50. package/dist/src/profile-loader.js +45 -24
  51. package/dist/src/profile-loader.js.map +1 -1
  52. package/dist/src/testing/fixtures.d.ts +32 -0
  53. package/dist/src/testing/fixtures.d.ts.map +1 -1
  54. package/dist/src/testing/fixtures.js +26 -0
  55. package/dist/src/testing/fixtures.js.map +1 -1
  56. package/dist/src/testing/mock-gitlab-server.d.ts.map +1 -1
  57. package/dist/src/testing/mock-gitlab-server.js +131 -1
  58. package/dist/src/testing/mock-gitlab-server.js.map +1 -1
  59. package/dist/src/types/http-transport.d.ts +16 -0
  60. package/dist/src/types/http-transport.d.ts.map +1 -1
  61. package/dist/src/types/openapi.d.ts +5 -0
  62. package/dist/src/types/openapi.d.ts.map +1 -1
  63. package/dist/src/types/profile.d.ts +112 -3
  64. package/dist/src/types/profile.d.ts.map +1 -1
  65. package/dist/src/validation-utils.d.ts +12 -0
  66. package/dist/src/validation-utils.d.ts.map +1 -1
  67. package/dist/src/validation-utils.js +17 -0
  68. package/dist/src/validation-utils.js.map +1 -1
  69. package/package.json +7 -3
  70. package/profile-schema.json +169 -7
  71. package/dist/composite-executor.d.ts +0 -65
  72. package/dist/composite-executor.d.ts.map +0 -1
  73. package/dist/composite-executor.js +0 -147
  74. package/dist/composite-executor.js.map +0 -1
  75. package/dist/constants.d.ts +0 -36
  76. package/dist/constants.d.ts.map +0 -1
  77. package/dist/constants.js +0 -36
  78. package/dist/constants.js.map +0 -1
  79. package/dist/http-transport.d.ts +0 -195
  80. package/dist/http-transport.d.ts.map +0 -1
  81. package/dist/http-transport.js +0 -760
  82. package/dist/http-transport.js.map +0 -1
  83. package/dist/interceptors.d.ts +0 -74
  84. package/dist/interceptors.d.ts.map +0 -1
  85. package/dist/interceptors.js +0 -220
  86. package/dist/interceptors.js.map +0 -1
  87. package/dist/logger.d.ts +0 -81
  88. package/dist/logger.d.ts.map +0 -1
  89. package/dist/logger.js +0 -264
  90. package/dist/logger.js.map +0 -1
  91. package/dist/mcp-server.d.ts +0 -110
  92. package/dist/mcp-server.d.ts.map +0 -1
  93. package/dist/mcp-server.js +0 -568
  94. package/dist/mcp-server.js.map +0 -1
  95. package/dist/metrics.d.ts +0 -86
  96. package/dist/metrics.d.ts.map +0 -1
  97. package/dist/metrics.js +0 -229
  98. package/dist/metrics.js.map +0 -1
  99. package/dist/openapi-parser.d.ts +0 -35
  100. package/dist/openapi-parser.d.ts.map +0 -1
  101. package/dist/openapi-parser.js +0 -160
  102. package/dist/openapi-parser.js.map +0 -1
  103. package/dist/profile-loader.d.ts +0 -25
  104. package/dist/profile-loader.d.ts.map +0 -1
  105. package/dist/profile-loader.js +0 -134
  106. package/dist/profile-loader.js.map +0 -1
  107. package/dist/schema-validator.d.ts +0 -32
  108. package/dist/schema-validator.d.ts.map +0 -1
  109. package/dist/schema-validator.js +0 -126
  110. package/dist/schema-validator.js.map +0 -1
  111. package/dist/testing/fixtures.d.ts +0 -186
  112. package/dist/testing/fixtures.d.ts.map +0 -1
  113. package/dist/testing/fixtures.js +0 -135
  114. package/dist/testing/fixtures.js.map +0 -1
  115. package/dist/testing/http-integration.test.d.ts +0 -7
  116. package/dist/testing/http-integration.test.d.ts.map +0 -1
  117. package/dist/testing/http-integration.test.js +0 -383
  118. package/dist/testing/http-integration.test.js.map +0 -1
  119. package/dist/testing/http-multiuser.test.d.ts +0 -10
  120. package/dist/testing/http-multiuser.test.d.ts.map +0 -1
  121. package/dist/testing/http-multiuser.test.js +0 -255
  122. package/dist/testing/http-multiuser.test.js.map +0 -1
  123. package/dist/testing/integration.test.d.ts +0 -8
  124. package/dist/testing/integration.test.d.ts.map +0 -1
  125. package/dist/testing/integration.test.js +0 -247
  126. package/dist/testing/integration.test.js.map +0 -1
  127. package/dist/testing/mock-gitlab-server.d.ts +0 -34
  128. package/dist/testing/mock-gitlab-server.d.ts.map +0 -1
  129. package/dist/testing/mock-gitlab-server.js +0 -224
  130. package/dist/testing/mock-gitlab-server.js.map +0 -1
  131. package/dist/testing/test-types.d.ts +0 -59
  132. package/dist/testing/test-types.d.ts.map +0 -1
  133. package/dist/testing/test-types.js +0 -7
  134. package/dist/testing/test-types.js.map +0 -1
  135. package/dist/tool-generator.d.ts +0 -43
  136. package/dist/tool-generator.d.ts.map +0 -1
  137. package/dist/tool-generator.js +0 -123
  138. package/dist/tool-generator.js.map +0 -1
  139. package/dist/tsconfig.tsbuildinfo +0 -1
  140. package/dist/types/http-transport.d.ts +0 -39
  141. package/dist/types/http-transport.d.ts.map +0 -1
  142. package/dist/types/http-transport.js +0 -8
  143. package/dist/types/http-transport.js.map +0 -1
  144. package/dist/types/openapi.d.ts +0 -50
  145. package/dist/types/openapi.d.ts.map +0 -1
  146. package/dist/types/openapi.js +0 -9
  147. package/dist/types/openapi.js.map +0 -1
  148. package/dist/types/profile.d.ts +0 -76
  149. package/dist/types/profile.d.ts.map +0 -1
  150. package/dist/types/profile.js +0 -9
  151. package/dist/types/profile.js.map +0 -1
@@ -1,760 +0,0 @@
1
- /**
2
- * HTTP Streamable Transport for MCP
3
- *
4
- * Implements MCP Specification 2025-03-26
5
- * https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
6
- *
7
- * Why: Enables remote MCP server access with SSE streaming, session management,
8
- * and resumability for reliable communication over HTTP.
9
- */
10
- import express from 'express';
11
- import crypto from 'crypto';
12
- import { MetricsCollector } from './metrics.js';
13
- export class HttpTransport {
14
- app;
15
- server = null;
16
- sessions = new Map();
17
- config;
18
- logger;
19
- metrics = null;
20
- cleanupInterval = null;
21
- messageHandler = null;
22
- constructor(config, logger) {
23
- this.config = config;
24
- this.logger = logger;
25
- // Initialize metrics if enabled
26
- if (config.metricsEnabled) {
27
- this.metrics = new MetricsCollector({
28
- enabled: true,
29
- prefix: 'mcp_',
30
- });
31
- }
32
- this.app = express();
33
- this.setupMiddleware();
34
- this.setupRoutes();
35
- }
36
- /**
37
- * Setup Express middleware
38
- *
39
- * Why: Security (Origin validation), JSON parsing, session extraction, metrics
40
- */
41
- setupMiddleware() {
42
- // JSON body parser
43
- this.app.use(express.json());
44
- // Metrics: Track request start time
45
- this.app.use((req, res, next) => {
46
- req.startTime = Date.now();
47
- next();
48
- });
49
- // Security: Origin validation (DNS rebinding protection)
50
- this.app.use((req, res, next) => {
51
- const origin = req.headers.origin;
52
- // Warn if binding to 0.0.0.0
53
- if (this.config.host === '0.0.0.0' && !this.hasWarnedAboutBinding) {
54
- this.logger.warn('HTTP transport bound to 0.0.0.0 - accessible from network. Ensure firewall protection.');
55
- this.hasWarnedAboutBinding = true;
56
- }
57
- // Skip Origin check for localhost
58
- if (req.hostname === 'localhost' || req.hostname === '127.0.0.1') {
59
- return next();
60
- }
61
- // Validate Origin header for non-localhost
62
- if (origin && !this.isAllowedOrigin(origin)) {
63
- this.logger.warn('Rejected request from disallowed origin', { origin, ip: req.ip });
64
- return res.status(403).json({
65
- error: 'Forbidden',
66
- message: 'Origin not allowed'
67
- });
68
- }
69
- next();
70
- });
71
- // Extract session ID from header
72
- this.app.use((req, res, next) => {
73
- const sessionId = req.headers['mcp-session-id'];
74
- if (sessionId) {
75
- req.sessionId = sessionId;
76
- }
77
- next();
78
- });
79
- }
80
- hasWarnedAboutBinding = false;
81
- /**
82
- * Check if origin is allowed
83
- *
84
- * Why: Prevent DNS rebinding attacks
85
- *
86
- * Supports:
87
- * - Exact hostname: 'example.com', 'api.example.com'
88
- * - Wildcard subdomain: '*.example.com'
89
- * - IPv4 CIDR: '192.168.1.0/24', '10.0.0.0/8'
90
- * - IPv4 exact: '192.168.1.100'
91
- */
92
- isAllowedOrigin(origin) {
93
- try {
94
- const url = new URL(origin);
95
- const hostname = url.hostname;
96
- // Always allow localhost
97
- if (hostname === 'localhost' || hostname === '127.0.0.1') {
98
- return true;
99
- }
100
- // Allow configured host
101
- if (hostname === this.config.host) {
102
- return true;
103
- }
104
- // Check custom allowed origins
105
- if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) {
106
- for (const allowed of this.config.allowedOrigins) {
107
- if (this.matchOrigin(hostname, allowed)) {
108
- return true;
109
- }
110
- }
111
- }
112
- return false;
113
- }
114
- catch {
115
- return false;
116
- }
117
- }
118
- /**
119
- * Match hostname against allowed origin pattern
120
- *
121
- * Supports:
122
- * - Exact match: 'example.com' === 'example.com'
123
- * - Wildcard: '*.example.com' matches 'api.example.com', 'web.example.com'
124
- * - CIDR: '192.168.1.0/24' matches '192.168.1.1' through '192.168.1.254'
125
- */
126
- matchOrigin(hostname, pattern) {
127
- // Exact match
128
- if (hostname === pattern) {
129
- return true;
130
- }
131
- // Wildcard subdomain match (*.example.com)
132
- if (pattern.startsWith('*.')) {
133
- const domain = pattern.substring(2); // Remove '*.'
134
- return hostname.endsWith('.' + domain) || hostname === domain;
135
- }
136
- // CIDR match (IPv4 only)
137
- if (pattern.includes('/')) {
138
- return this.matchCIDR(hostname, pattern);
139
- }
140
- return false;
141
- }
142
- /**
143
- * Check if IP address is within CIDR range
144
- *
145
- * Example: '192.168.1.50' matches '192.168.1.0/24'
146
- */
147
- matchCIDR(ip, cidr) {
148
- // Parse CIDR
149
- const [range, bits] = cidr.split('/');
150
- const maskBits = parseInt(bits, 10);
151
- if (isNaN(maskBits) || maskBits < 0 || maskBits > 32) {
152
- this.logger.warn('Invalid CIDR mask bits', { cidr });
153
- return false;
154
- }
155
- // Convert IP addresses to 32-bit integers
156
- const ipInt = this.ipToInt(ip);
157
- const rangeInt = this.ipToInt(range);
158
- if (ipInt === null || rangeInt === null) {
159
- return false;
160
- }
161
- // Create mask (e.g., /24 = 0xFFFFFF00)
162
- const mask = (0xFFFFFFFF << (32 - maskBits)) >>> 0;
163
- // Compare network portions
164
- return (ipInt & mask) === (rangeInt & mask);
165
- }
166
- /**
167
- * Convert IPv4 address to 32-bit integer
168
- *
169
- * Example: '192.168.1.1' -> 3232235777
170
- */
171
- ipToInt(ip) {
172
- const parts = ip.split('.');
173
- if (parts.length !== 4) {
174
- return null;
175
- }
176
- let result = 0;
177
- for (let i = 0; i < 4; i++) {
178
- const octet = parseInt(parts[i], 10);
179
- if (isNaN(octet) || octet < 0 || octet > 255) {
180
- return null;
181
- }
182
- result = (result << 8) | octet;
183
- }
184
- return result >>> 0; // Unsigned
185
- }
186
- /**
187
- * Setup MCP endpoint routes
188
- *
189
- * Why: Single endpoint for POST (client→server) and GET (SSE stream)
190
- */
191
- setupRoutes() {
192
- // Main MCP endpoint - POST for sending messages
193
- this.app.post('/mcp', this.handlePost.bind(this));
194
- // Main MCP endpoint - GET for SSE streaming
195
- this.app.get('/mcp', this.handleGet.bind(this));
196
- // Session termination
197
- this.app.delete('/mcp', this.handleDelete.bind(this));
198
- // Metrics endpoint (if enabled)
199
- if (this.config.metricsEnabled) {
200
- this.app.get(this.config.metricsPath, this.handleMetrics.bind(this));
201
- }
202
- // Health check
203
- this.app.get('/health', (req, res) => {
204
- const startTime = Date.now();
205
- res.json({ status: 'ok', sessions: this.sessions.size });
206
- if (this.metrics) {
207
- const duration = (Date.now() - startTime) / 1000;
208
- this.metrics.recordHttpRequest(req.method, req.path, res.statusCode, duration);
209
- }
210
- });
211
- }
212
- /**
213
- * Handle metrics endpoint
214
- *
215
- * Why: Prometheus scraping endpoint
216
- */
217
- async handleMetrics(req, res) {
218
- const startTime = Date.now();
219
- try {
220
- if (!this.metrics) {
221
- res.status(404).json({ error: 'Not Found', message: 'Metrics disabled' });
222
- return;
223
- }
224
- const metrics = await this.metrics.getMetrics();
225
- res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
226
- res.send(metrics);
227
- // Don't record metrics call in metrics (avoid recursion)
228
- }
229
- catch (error) {
230
- this.logger.error('Metrics endpoint error', error);
231
- res.status(500).json({ error: 'Internal Server Error', message: error.message });
232
- }
233
- }
234
- /**
235
- * Validate token format and length
236
- *
237
- * Why centralized: Single source of truth for token validation rules
238
- */
239
- validateToken(token, source) {
240
- if (token.length > 1000) {
241
- throw new Error(`${source} too long (max 1000 characters)`);
242
- }
243
- // RFC 6750 Bearer token characters + common API token chars
244
- if (!/^[A-Za-z0-9\-._~+/]+=*$/.test(token)) {
245
- throw new Error(`Invalid ${source} format`);
246
- }
247
- }
248
- /**
249
- * Extract and validate auth token from request headers
250
- *
251
- * Supports:
252
- * - Authorization: Bearer <token>
253
- * - X-API-Token: <token>
254
- *
255
- * Why strict validation: Prevents header injection attacks
256
- */
257
- extractAuthToken(req) {
258
- const authHeader = req.headers.authorization;
259
- if (authHeader) {
260
- // Strict Bearer token format validation
261
- const match = authHeader.match(/^Bearer\s+([A-Za-z0-9\-._~+/]+=*)$/);
262
- if (!match) {
263
- throw new Error('Invalid Authorization header format. Expected: Bearer <token>');
264
- }
265
- const token = match[1];
266
- this.validateToken(token, 'Authorization token');
267
- return token;
268
- }
269
- const apiTokenHeader = req.headers['x-api-token'];
270
- if (apiTokenHeader) {
271
- if (typeof apiTokenHeader !== 'string') {
272
- throw new Error('X-API-Token must be a string');
273
- }
274
- this.validateToken(apiTokenHeader, 'X-API-Token');
275
- return apiTokenHeader;
276
- }
277
- return undefined;
278
- }
279
- /**
280
- * Handle POST requests - Client sending messages to server
281
- *
282
- * MCP Spec: POST can contain requests, notifications, or responses
283
- */
284
- async handlePost(req, res) {
285
- const startTime = Date.now();
286
- try {
287
- const sessionId = req.sessionId;
288
- const body = req.body;
289
- // Validate Accept header
290
- const accept = req.headers.accept || '';
291
- if (!accept.includes('application/json') && !accept.includes('text/event-stream')) {
292
- res.status(406).json({ error: 'Not Acceptable', message: 'Must accept application/json or text/event-stream' });
293
- return;
294
- }
295
- // Check if this is initialization (no session ID yet)
296
- const isInitialization = this.isInitializeRequest(body);
297
- // Validate session (except for initialization)
298
- if (!isInitialization && sessionId) {
299
- const session = this.sessions.get(sessionId);
300
- if (!session) {
301
- res.status(404).json({ error: 'Not Found', message: 'Session not found or expired' });
302
- return;
303
- }
304
- this.updateSessionActivity(sessionId);
305
- }
306
- else if (!isInitialization && !sessionId) {
307
- res.status(400).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required (except for initialization)' });
308
- return;
309
- }
310
- // Determine message type
311
- const messageType = this.getMessageType(body);
312
- // If only notifications/responses, return 202 Accepted
313
- if (messageType === 'notification-only' || messageType === 'response-only') {
314
- if (this.messageHandler) {
315
- await this.messageHandler(body);
316
- }
317
- res.status(202).send();
318
- return;
319
- }
320
- // If contains requests, process and return response
321
- if (messageType === 'request') {
322
- if (!this.messageHandler) {
323
- res.status(500).json({ error: 'Internal Server Error', message: 'Message handler not configured' });
324
- return;
325
- }
326
- const response = await this.messageHandler(body, sessionId);
327
- // Create session on initialization
328
- let newSessionId;
329
- if (isInitialization) {
330
- // Extract and validate auth token from headers
331
- const authToken = this.extractAuthToken(req);
332
- newSessionId = this.createSession(authToken);
333
- }
334
- // Check if client prefers SSE stream
335
- if (accept.includes('text/event-stream')) {
336
- // Return SSE stream
337
- this.startSSEResponse(res, response, newSessionId, sessionId);
338
- }
339
- else {
340
- // Return JSON
341
- if (newSessionId) {
342
- res.setHeader('Mcp-Session-Id', newSessionId);
343
- }
344
- res.json(response);
345
- }
346
- return;
347
- }
348
- res.status(400).json({ error: 'Bad Request', message: 'Invalid message type' });
349
- }
350
- catch (error) {
351
- this.logger.error('POST request error', error);
352
- const status = 500;
353
- res.status(status).json({ error: 'Internal Server Error', message: error.message });
354
- // Record error metrics
355
- if (this.metrics) {
356
- const duration = (Date.now() - startTime) / 1000;
357
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
358
- }
359
- }
360
- finally {
361
- // Record success metrics (if not already recorded in catch)
362
- if (this.metrics && res.statusCode !== 500) {
363
- const duration = (Date.now() - startTime) / 1000;
364
- this.metrics.recordHttpRequest(req.method, req.path, res.statusCode, duration);
365
- }
366
- }
367
- }
368
- /**
369
- * Handle GET requests - Client opening SSE stream for server messages
370
- *
371
- * MCP Spec: GET opens SSE stream for server-initiated requests/notifications
372
- */
373
- async handleGet(req, res) {
374
- const startTime = Date.now();
375
- try {
376
- const sessionId = req.sessionId;
377
- const lastEventId = req.headers['last-event-id'];
378
- // Validate Accept header
379
- const accept = req.headers.accept || '';
380
- if (!accept.includes('text/event-stream')) {
381
- res.status(405).json({ error: 'Method Not Allowed', message: 'Must accept text/event-stream' });
382
- return;
383
- }
384
- // Validate session
385
- if (!sessionId) {
386
- res.status(400).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required' });
387
- return;
388
- }
389
- const session = this.sessions.get(sessionId);
390
- if (!session) {
391
- res.status(404).json({ error: 'Not Found', message: 'Session not found or expired' });
392
- return;
393
- }
394
- this.updateSessionActivity(sessionId);
395
- // Start SSE stream
396
- this.startSSEStream(res, sessionId, lastEventId);
397
- // Record metrics for successful SSE start
398
- if (this.metrics) {
399
- const duration = (Date.now() - startTime) / 1000;
400
- this.metrics.recordHttpRequest(req.method, req.path, 200, duration);
401
- }
402
- }
403
- catch (error) {
404
- this.logger.error('GET request error', error);
405
- const status = 500;
406
- if (!res.headersSent) {
407
- res.status(status).json({ error: 'Internal Server Error', message: error.message });
408
- }
409
- // Record error metrics
410
- if (this.metrics) {
411
- const duration = (Date.now() - startTime) / 1000;
412
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
413
- }
414
- }
415
- }
416
- /**
417
- * Handle DELETE requests - Client terminating session
418
- *
419
- * MCP Spec: DELETE explicitly terminates session
420
- */
421
- handleDelete(req, res) {
422
- const startTime = Date.now();
423
- const sessionId = req.sessionId;
424
- if (!sessionId) {
425
- const status = 400;
426
- res.status(status).json({ error: 'Bad Request', message: 'Mcp-Session-Id header required' });
427
- if (this.metrics) {
428
- const duration = (Date.now() - startTime) / 1000;
429
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
430
- }
431
- return;
432
- }
433
- const session = this.sessions.get(sessionId);
434
- if (!session) {
435
- const status = 404;
436
- res.status(status).json({ error: 'Not Found', message: 'Session not found' });
437
- if (this.metrics) {
438
- const duration = (Date.now() - startTime) / 1000;
439
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
440
- }
441
- return;
442
- }
443
- this.destroySession(sessionId);
444
- const status = 204;
445
- res.status(status).send();
446
- if (this.metrics) {
447
- const duration = (Date.now() - startTime) / 1000;
448
- this.metrics.recordHttpRequest(req.method, req.path, status, duration);
449
- }
450
- }
451
- /**
452
- * Start SSE response for a POST request
453
- *
454
- * Why: Returns response via SSE stream, allows server-initiated messages
455
- */
456
- startSSEResponse(res, response, newSessionId, sessionId) {
457
- res.setHeader('Content-Type', 'text/event-stream');
458
- res.setHeader('Cache-Control', 'no-cache');
459
- res.setHeader('Connection', 'keep-alive');
460
- if (newSessionId) {
461
- res.setHeader('Mcp-Session-Id', newSessionId);
462
- }
463
- // Send response
464
- const eventId = Date.now();
465
- res.write(`id: ${eventId}\n`);
466
- res.write(`data: ${JSON.stringify(response)}\n\n`);
467
- // Close stream
468
- res.end();
469
- }
470
- /**
471
- * Start SSE stream for GET request
472
- *
473
- * Why: Allows server to send requests/notifications to client
474
- */
475
- startSSEStream(res, sessionId, lastEventId) {
476
- res.setHeader('Content-Type', 'text/event-stream');
477
- res.setHeader('Cache-Control', 'no-cache');
478
- res.setHeader('Connection', 'keep-alive');
479
- const streamId = crypto.randomBytes(16).toString('hex');
480
- const session = this.sessions.get(sessionId);
481
- const streamState = {
482
- streamId,
483
- lastEventId: lastEventId ? parseInt(lastEventId, 10) : 0,
484
- messageQueue: [],
485
- active: true,
486
- };
487
- session.sseStreams.set(streamId, streamState);
488
- // Replay missed messages if resuming
489
- if (lastEventId) {
490
- this.replayMessages(res, streamState);
491
- }
492
- // Setup heartbeat if enabled
493
- let heartbeatInterval = null;
494
- if (this.config.heartbeatEnabled) {
495
- heartbeatInterval = setInterval(() => {
496
- if (streamState.active) {
497
- res.write(':ping\n\n');
498
- }
499
- }, this.config.heartbeatIntervalMs);
500
- }
501
- // Handle client disconnect
502
- res.on('close', () => {
503
- streamState.active = false;
504
- if (heartbeatInterval) {
505
- clearInterval(heartbeatInterval);
506
- }
507
- this.logger.info('SSE stream closed', { sessionId, streamId });
508
- });
509
- this.logger.info('SSE stream opened', { sessionId, streamId, resuming: !!lastEventId });
510
- }
511
- /**
512
- * Replay messages after Last-Event-ID
513
- *
514
- * Why: Resumability - client can reconnect and receive missed messages
515
- */
516
- replayMessages(res, streamState) {
517
- const missedMessages = streamState.messageQueue.filter(msg => msg.eventId > streamState.lastEventId);
518
- for (const msg of missedMessages) {
519
- res.write(`id: ${msg.eventId}\n`);
520
- res.write(`data: ${JSON.stringify(msg.data)}\n\n`);
521
- }
522
- this.logger.info('Replayed messages', { count: missedMessages.length, streamId: streamState.streamId });
523
- }
524
- /**
525
- * Send message to client via SSE
526
- *
527
- * Why: Server-initiated requests/notifications
528
- */
529
- sendToClient(sessionId, message) {
530
- const session = this.sessions.get(sessionId);
531
- if (!session) {
532
- this.logger.warn('Cannot send to client: session not found', { sessionId });
533
- return;
534
- }
535
- const eventId = Date.now();
536
- const queuedMessage = {
537
- eventId,
538
- data: message,
539
- timestamp: Date.now(),
540
- };
541
- // Send to all active streams for this session
542
- for (const [streamId, streamState] of session.sseStreams) {
543
- if (streamState.active) {
544
- // Queue for resumability
545
- streamState.messageQueue.push(queuedMessage);
546
- // Keep only last 100 messages
547
- if (streamState.messageQueue.length > 100) {
548
- streamState.messageQueue.shift();
549
- }
550
- }
551
- }
552
- }
553
- /**
554
- * Check if request is initialization
555
- */
556
- isInitializeRequest(body) {
557
- if (typeof body !== 'object' || body === null)
558
- return false;
559
- const req = body;
560
- return req.method === 'initialize';
561
- }
562
- /**
563
- * Determine message type (request, notification, response)
564
- */
565
- getMessageType(body) {
566
- if (Array.isArray(body)) {
567
- // Batch
568
- const hasRequest = body.some((msg) => typeof msg === 'object' && msg !== null && 'method' in msg && 'id' in msg);
569
- const hasNotification = body.some((msg) => typeof msg === 'object' && msg !== null && 'method' in msg && !('id' in msg));
570
- const hasResponse = body.some((msg) => typeof msg === 'object' && msg !== null && ('result' in msg || 'error' in msg));
571
- if (hasRequest)
572
- return 'request';
573
- if (hasNotification && !hasResponse)
574
- return 'notification-only';
575
- if (hasResponse && !hasNotification)
576
- return 'response-only';
577
- return 'mixed';
578
- }
579
- else if (typeof body === 'object' && body !== null) {
580
- const msg = body;
581
- if ('method' in msg) {
582
- return 'id' in msg ? 'request' : 'notification-only';
583
- }
584
- if ('result' in msg || 'error' in msg) {
585
- return 'response-only';
586
- }
587
- }
588
- return 'unknown';
589
- }
590
- /**
591
- * Create new session
592
- *
593
- * Why: Stateful sessions for MCP protocol
594
- */
595
- createSession(authToken) {
596
- // Validate token if provided (defense in depth)
597
- if (authToken) {
598
- this.validateToken(authToken, 'Session auth token');
599
- }
600
- const sessionId = crypto.randomUUID();
601
- const session = {
602
- id: sessionId,
603
- createdAt: Date.now(),
604
- lastActivityAt: Date.now(),
605
- sseStreams: new Map(),
606
- authToken,
607
- };
608
- this.sessions.set(sessionId, session);
609
- this.logger.info('Session created', { sessionId, hasAuthToken: !!authToken });
610
- // Record metrics
611
- if (this.metrics) {
612
- this.metrics.recordSessionCreated();
613
- }
614
- return sessionId;
615
- }
616
- /**
617
- * Update session activity timestamp
618
- */
619
- updateSessionActivity(sessionId) {
620
- const session = this.sessions.get(sessionId);
621
- if (session) {
622
- session.lastActivityAt = Date.now();
623
- }
624
- }
625
- /**
626
- * Destroy session and cleanup resources
627
- *
628
- * Why: Free memory, close streams
629
- */
630
- destroySession(sessionId) {
631
- const session = this.sessions.get(sessionId);
632
- if (session) {
633
- // Close all active SSE streams
634
- for (const [, streamState] of session.sseStreams) {
635
- streamState.active = false;
636
- }
637
- session.sseStreams.clear();
638
- this.sessions.delete(sessionId);
639
- this.logger.info('Session destroyed', { sessionId });
640
- // Notify session destruction listeners (for cleanup in MCPServer)
641
- this.notifySessionDestroyed(sessionId);
642
- // Record metrics
643
- if (this.metrics) {
644
- this.metrics.recordSessionDestroyed();
645
- }
646
- }
647
- }
648
- /**
649
- * Session destruction listeners for cleanup in other components
650
- */
651
- sessionDestroyedListeners = [];
652
- /**
653
- * Register listener for session destruction events
654
- *
655
- * Why: Allows MCPServer to cleanup per-session HTTP clients
656
- */
657
- onSessionDestroyed(listener) {
658
- this.sessionDestroyedListeners.push(listener);
659
- }
660
- /**
661
- * Notify all listeners about session destruction
662
- */
663
- notifySessionDestroyed(sessionId) {
664
- for (const listener of this.sessionDestroyedListeners) {
665
- try {
666
- listener(sessionId);
667
- }
668
- catch (error) {
669
- this.logger.error('Session destroyed listener error', error);
670
- }
671
- }
672
- }
673
- /**
674
- * Cleanup expired sessions
675
- *
676
- * Why: Prevent memory leaks, enforce session timeout
677
- */
678
- cleanupExpiredSessions() {
679
- const now = Date.now();
680
- const expiredSessions = [];
681
- for (const [sessionId, session] of this.sessions) {
682
- const age = now - session.lastActivityAt;
683
- if (age > this.config.sessionTimeoutMs) {
684
- expiredSessions.push(sessionId);
685
- }
686
- }
687
- for (const sessionId of expiredSessions) {
688
- this.destroySession(sessionId);
689
- }
690
- if (expiredSessions.length > 0) {
691
- this.logger.info('Cleaned up expired sessions', { count: expiredSessions.length });
692
- }
693
- }
694
- /**
695
- * Get auth token from session
696
- *
697
- * Why public: Allows MCPServer to securely access session tokens without breaking encapsulation
698
- */
699
- getSessionToken(sessionId) {
700
- const session = this.sessions.get(sessionId);
701
- return session?.authToken;
702
- }
703
- /**
704
- * Set message handler for processing incoming JSON-RPC messages
705
- */
706
- setMessageHandler(handler) {
707
- this.messageHandler = handler;
708
- }
709
- /**
710
- * Start HTTP server
711
- */
712
- async start() {
713
- return new Promise((resolve, reject) => {
714
- try {
715
- this.server = this.app.listen(this.config.port, this.config.host, () => {
716
- this.logger.info('HTTP transport started', {
717
- host: this.config.host,
718
- port: this.config.port,
719
- heartbeat: this.config.heartbeatEnabled,
720
- metrics: this.config.metricsEnabled,
721
- });
722
- // Start session cleanup interval
723
- this.cleanupInterval = setInterval(() => this.cleanupExpiredSessions(), 60000 // Check every minute
724
- );
725
- resolve();
726
- });
727
- this.server.on('error', reject);
728
- }
729
- catch (error) {
730
- reject(error);
731
- }
732
- });
733
- }
734
- /**
735
- * Stop HTTP server
736
- */
737
- async stop() {
738
- if (this.cleanupInterval) {
739
- clearInterval(this.cleanupInterval);
740
- this.cleanupInterval = null;
741
- }
742
- // Destroy all sessions
743
- for (const sessionId of this.sessions.keys()) {
744
- this.destroySession(sessionId);
745
- }
746
- if (this.server) {
747
- return new Promise((resolve, reject) => {
748
- this.server.close((err) => {
749
- if (err)
750
- reject(err);
751
- else {
752
- this.logger.info('HTTP transport stopped');
753
- resolve();
754
- }
755
- });
756
- });
757
- }
758
- }
759
- }
760
- //# sourceMappingURL=http-transport.js.map