tracelattice 1.2.5

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 (294) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +112 -0
  3. package/dist/ServerConfig.d.ts +229 -0
  4. package/dist/ServerConfig.d.ts.map +1 -0
  5. package/dist/ServerConfig.js +121 -0
  6. package/dist/ServerConfig.js.map +1 -0
  7. package/dist/__tests__/base-registry.test.d.ts +2 -0
  8. package/dist/__tests__/base-registry.test.d.ts.map +1 -0
  9. package/dist/__tests__/base-transport-cov.test.d.ts +2 -0
  10. package/dist/__tests__/base-transport-cov.test.d.ts.map +1 -0
  11. package/dist/__tests__/base-transport.test.d.ts +2 -0
  12. package/dist/__tests__/base-transport.test.d.ts.map +1 -0
  13. package/dist/__tests__/config-loader.test.d.ts +2 -0
  14. package/dist/__tests__/config-loader.test.d.ts.map +1 -0
  15. package/dist/__tests__/connection-pool-cov.test.d.ts +2 -0
  16. package/dist/__tests__/connection-pool-cov.test.d.ts.map +1 -0
  17. package/dist/__tests__/connection-pool.test.d.ts +2 -0
  18. package/dist/__tests__/connection-pool.test.d.ts.map +1 -0
  19. package/dist/__tests__/container.test.d.ts +2 -0
  20. package/dist/__tests__/container.test.d.ts.map +1 -0
  21. package/dist/__tests__/crud.test.d.ts +2 -0
  22. package/dist/__tests__/crud.test.d.ts.map +1 -0
  23. package/dist/__tests__/discovery-cache.test.d.ts +2 -0
  24. package/dist/__tests__/discovery-cache.test.d.ts.map +1 -0
  25. package/dist/__tests__/errors.test.d.ts +2 -0
  26. package/dist/__tests__/errors.test.d.ts.map +1 -0
  27. package/dist/__tests__/factories.test.d.ts +2 -0
  28. package/dist/__tests__/factories.test.d.ts.map +1 -0
  29. package/dist/__tests__/health-checker-cov.test.d.ts +2 -0
  30. package/dist/__tests__/health-checker-cov.test.d.ts.map +1 -0
  31. package/dist/__tests__/health-checker.test.d.ts +2 -0
  32. package/dist/__tests__/health-checker.test.d.ts.map +1 -0
  33. package/dist/__tests__/helpers/factories.d.ts +36 -0
  34. package/dist/__tests__/helpers/factories.d.ts.map +1 -0
  35. package/dist/__tests__/helpers/index.d.ts +3 -0
  36. package/dist/__tests__/helpers/index.d.ts.map +1 -0
  37. package/dist/__tests__/helpers/timers.d.ts +4 -0
  38. package/dist/__tests__/helpers/timers.d.ts.map +1 -0
  39. package/dist/__tests__/history-manager.test.d.ts +2 -0
  40. package/dist/__tests__/history-manager.test.d.ts.map +1 -0
  41. package/dist/__tests__/http-helpers-cov.test.d.ts +2 -0
  42. package/dist/__tests__/http-helpers-cov.test.d.ts.map +1 -0
  43. package/dist/__tests__/http-transport-cov.test.d.ts +2 -0
  44. package/dist/__tests__/http-transport-cov.test.d.ts.map +1 -0
  45. package/dist/__tests__/http-transport.test.d.ts +2 -0
  46. package/dist/__tests__/http-transport.test.d.ts.map +1 -0
  47. package/dist/__tests__/input-normalizer.test.d.ts +8 -0
  48. package/dist/__tests__/input-normalizer.test.d.ts.map +1 -0
  49. package/dist/__tests__/integration.test.d.ts +2 -0
  50. package/dist/__tests__/integration.test.d.ts.map +1 -0
  51. package/dist/__tests__/lib-server.test.d.ts +2 -0
  52. package/dist/__tests__/lib-server.test.d.ts.map +1 -0
  53. package/dist/__tests__/memory-persistence.test.d.ts +2 -0
  54. package/dist/__tests__/memory-persistence.test.d.ts.map +1 -0
  55. package/dist/__tests__/metrics-integration.test.d.ts +2 -0
  56. package/dist/__tests__/metrics-integration.test.d.ts.map +1 -0
  57. package/dist/__tests__/persistence.test.d.ts +2 -0
  58. package/dist/__tests__/persistence.test.d.ts.map +1 -0
  59. package/dist/__tests__/reasoning-integration.test.d.ts +11 -0
  60. package/dist/__tests__/reasoning-integration.test.d.ts.map +1 -0
  61. package/dist/__tests__/reasoning-types.test.d.ts +2 -0
  62. package/dist/__tests__/reasoning-types.test.d.ts.map +1 -0
  63. package/dist/__tests__/request-context.test.d.ts +2 -0
  64. package/dist/__tests__/request-context.test.d.ts.map +1 -0
  65. package/dist/__tests__/sanitize.test.d.ts +2 -0
  66. package/dist/__tests__/sanitize.test.d.ts.map +1 -0
  67. package/dist/__tests__/schema.test.d.ts +2 -0
  68. package/dist/__tests__/schema.test.d.ts.map +1 -0
  69. package/dist/__tests__/sequentialthinking-tools.test.d.ts +2 -0
  70. package/dist/__tests__/sequentialthinking-tools.test.d.ts.map +1 -0
  71. package/dist/__tests__/server-config.test.d.ts +2 -0
  72. package/dist/__tests__/server-config.test.d.ts.map +1 -0
  73. package/dist/__tests__/skill-discovery.test.d.ts +2 -0
  74. package/dist/__tests__/skill-discovery.test.d.ts.map +1 -0
  75. package/dist/__tests__/skill-registry.test.d.ts +2 -0
  76. package/dist/__tests__/skill-registry.test.d.ts.map +1 -0
  77. package/dist/__tests__/skill-watcher.test.d.ts +2 -0
  78. package/dist/__tests__/skill-watcher.test.d.ts.map +1 -0
  79. package/dist/__tests__/sqlite-persistence.test.d.ts +2 -0
  80. package/dist/__tests__/sqlite-persistence.test.d.ts.map +1 -0
  81. package/dist/__tests__/sse-transport-cov.test.d.ts +2 -0
  82. package/dist/__tests__/sse-transport-cov.test.d.ts.map +1 -0
  83. package/dist/__tests__/sse-transport.test.d.ts +2 -0
  84. package/dist/__tests__/sse-transport.test.d.ts.map +1 -0
  85. package/dist/__tests__/streamable-http-cov.test.d.ts +2 -0
  86. package/dist/__tests__/streamable-http-cov.test.d.ts.map +1 -0
  87. package/dist/__tests__/streamable-http-transport.test.d.ts +2 -0
  88. package/dist/__tests__/streamable-http-transport.test.d.ts.map +1 -0
  89. package/dist/__tests__/structured-logger.test.d.ts +2 -0
  90. package/dist/__tests__/structured-logger.test.d.ts.map +1 -0
  91. package/dist/__tests__/thought-evaluator.test.d.ts +2 -0
  92. package/dist/__tests__/thought-evaluator.test.d.ts.map +1 -0
  93. package/dist/__tests__/thought-formatter.test.d.ts +2 -0
  94. package/dist/__tests__/thought-formatter.test.d.ts.map +1 -0
  95. package/dist/__tests__/thought-processor.test.d.ts +8 -0
  96. package/dist/__tests__/thought-processor.test.d.ts.map +1 -0
  97. package/dist/__tests__/tool-registry-cov.test.d.ts +2 -0
  98. package/dist/__tests__/tool-registry-cov.test.d.ts.map +1 -0
  99. package/dist/__tests__/tool-registry.test.d.ts +2 -0
  100. package/dist/__tests__/tool-registry.test.d.ts.map +1 -0
  101. package/dist/__tests__/tool-watcher.test.d.ts +2 -0
  102. package/dist/__tests__/tool-watcher.test.d.ts.map +1 -0
  103. package/dist/__tests__/worker-manager-cov.test.d.ts +2 -0
  104. package/dist/__tests__/worker-manager-cov.test.d.ts.map +1 -0
  105. package/dist/__tests__/worker-manager.test.d.ts +2 -0
  106. package/dist/__tests__/worker-manager.test.d.ts.map +1 -0
  107. package/dist/cache/DiscoveryCache.d.ts +269 -0
  108. package/dist/cache/DiscoveryCache.d.ts.map +1 -0
  109. package/dist/cache/DiscoveryCache.js +100 -0
  110. package/dist/cache/DiscoveryCache.js.map +1 -0
  111. package/dist/cli.d.ts +3 -0
  112. package/dist/cli.d.ts.map +1 -0
  113. package/dist/cli.js +114 -0
  114. package/dist/cli.js.map +1 -0
  115. package/dist/cluster/WorkerManager.d.ts +166 -0
  116. package/dist/cluster/WorkerManager.d.ts.map +1 -0
  117. package/dist/cluster/WorkerManager.js +202 -0
  118. package/dist/cluster/WorkerManager.js.map +1 -0
  119. package/dist/cluster/worker.d.ts +11 -0
  120. package/dist/cluster/worker.d.ts.map +1 -0
  121. package/dist/cluster/worker.js +36 -0
  122. package/dist/cluster/worker.js.map +1 -0
  123. package/dist/config/ConfigLoader.d.ts +224 -0
  124. package/dist/config/ConfigLoader.d.ts.map +1 -0
  125. package/dist/config/ConfigLoader.js +85 -0
  126. package/dist/config/ConfigLoader.js.map +1 -0
  127. package/dist/context/RequestContext.d.ts +61 -0
  128. package/dist/context/RequestContext.d.ts.map +1 -0
  129. package/dist/context/RequestContext.js +17 -0
  130. package/dist/context/RequestContext.js.map +1 -0
  131. package/dist/contracts/index.d.ts +10 -0
  132. package/dist/contracts/index.d.ts.map +1 -0
  133. package/dist/contracts/index.js +1 -0
  134. package/dist/contracts/interfaces.d.ts +107 -0
  135. package/dist/contracts/interfaces.d.ts.map +1 -0
  136. package/dist/contracts/interfaces.js +1 -0
  137. package/dist/core/HistoryManager.d.ts +514 -0
  138. package/dist/core/HistoryManager.d.ts.map +1 -0
  139. package/dist/core/HistoryManager.js +331 -0
  140. package/dist/core/HistoryManager.js.map +1 -0
  141. package/dist/core/IHistoryManager.d.ts +100 -0
  142. package/dist/core/IHistoryManager.d.ts.map +1 -0
  143. package/dist/core/IHistoryManager.js +1 -0
  144. package/dist/core/InputNormalizer.d.ts +139 -0
  145. package/dist/core/InputNormalizer.d.ts.map +1 -0
  146. package/dist/core/InputNormalizer.js +101 -0
  147. package/dist/core/InputNormalizer.js.map +1 -0
  148. package/dist/core/ThoughtEvaluator.d.ts +127 -0
  149. package/dist/core/ThoughtEvaluator.d.ts.map +1 -0
  150. package/dist/core/ThoughtEvaluator.js +346 -0
  151. package/dist/core/ThoughtEvaluator.js.map +1 -0
  152. package/dist/core/ThoughtFormatter.d.ts +133 -0
  153. package/dist/core/ThoughtFormatter.d.ts.map +1 -0
  154. package/dist/core/ThoughtFormatter.js +70 -0
  155. package/dist/core/ThoughtFormatter.js.map +1 -0
  156. package/dist/core/ThoughtProcessor.d.ts +218 -0
  157. package/dist/core/ThoughtProcessor.d.ts.map +1 -0
  158. package/dist/core/ThoughtProcessor.js +205 -0
  159. package/dist/core/ThoughtProcessor.js.map +1 -0
  160. package/dist/core/reasoning.d.ts +169 -0
  161. package/dist/core/reasoning.d.ts.map +1 -0
  162. package/dist/core/reasoning.js +1 -0
  163. package/dist/core/step.d.ts +45 -0
  164. package/dist/core/step.d.ts.map +1 -0
  165. package/dist/core/step.js +1 -0
  166. package/dist/core/thought.d.ts +190 -0
  167. package/dist/core/thought.d.ts.map +1 -0
  168. package/dist/core/thought.js +1 -0
  169. package/dist/di/Container.d.ts +226 -0
  170. package/dist/di/Container.d.ts.map +1 -0
  171. package/dist/di/Container.js +96 -0
  172. package/dist/di/Container.js.map +1 -0
  173. package/dist/di/ServiceRegistry.d.ts +32 -0
  174. package/dist/di/ServiceRegistry.d.ts.map +1 -0
  175. package/dist/di/ServiceRegistry.js +1 -0
  176. package/dist/errors.d.ts +482 -0
  177. package/dist/errors.d.ts.map +1 -0
  178. package/dist/errors.js +108 -0
  179. package/dist/errors.js.map +1 -0
  180. package/dist/health/HealthChecker.d.ts +73 -0
  181. package/dist/health/HealthChecker.d.ts.map +1 -0
  182. package/dist/health/HealthChecker.js +69 -0
  183. package/dist/health/HealthChecker.js.map +1 -0
  184. package/dist/index.d.ts +2 -0
  185. package/dist/index.d.ts.map +1 -0
  186. package/dist/index.js +1 -0
  187. package/dist/lib.d.ts +205 -0
  188. package/dist/lib.d.ts.map +1 -0
  189. package/dist/lib.js +219 -0
  190. package/dist/lib.js.map +1 -0
  191. package/dist/logger/NullLogger.d.ts +154 -0
  192. package/dist/logger/NullLogger.d.ts.map +1 -0
  193. package/dist/logger/NullLogger.js +24 -0
  194. package/dist/logger/NullLogger.js.map +1 -0
  195. package/dist/logger/StructuredLogger.d.ts +327 -0
  196. package/dist/logger/StructuredLogger.d.ts.map +1 -0
  197. package/dist/logger/StructuredLogger.js +72 -0
  198. package/dist/logger/StructuredLogger.js.map +1 -0
  199. package/dist/metrics/__tests__/metrics.test.d.ts +2 -0
  200. package/dist/metrics/__tests__/metrics.test.d.ts.map +1 -0
  201. package/dist/metrics/metrics.impl.d.ts +252 -0
  202. package/dist/metrics/metrics.impl.d.ts.map +1 -0
  203. package/dist/metrics/metrics.impl.js +197 -0
  204. package/dist/metrics/metrics.impl.js.map +1 -0
  205. package/dist/persistence/FilePersistence.d.ts +66 -0
  206. package/dist/persistence/FilePersistence.d.ts.map +1 -0
  207. package/dist/persistence/FilePersistence.js +132 -0
  208. package/dist/persistence/FilePersistence.js.map +1 -0
  209. package/dist/persistence/MemoryPersistence.d.ts +68 -0
  210. package/dist/persistence/MemoryPersistence.d.ts.map +1 -0
  211. package/dist/persistence/MemoryPersistence.js +51 -0
  212. package/dist/persistence/MemoryPersistence.js.map +1 -0
  213. package/dist/persistence/PersistenceBackend.d.ts +69 -0
  214. package/dist/persistence/PersistenceBackend.d.ts.map +1 -0
  215. package/dist/persistence/PersistenceBackend.js +1 -0
  216. package/dist/persistence/PersistenceFactory.d.ts +21 -0
  217. package/dist/persistence/PersistenceFactory.d.ts.map +1 -0
  218. package/dist/persistence/PersistenceFactory.js +25 -0
  219. package/dist/persistence/PersistenceFactory.js.map +1 -0
  220. package/dist/persistence/SqlitePersistence.d.ts +60 -0
  221. package/dist/persistence/SqlitePersistence.d.ts.map +1 -0
  222. package/dist/persistence/SqlitePersistence.js +136 -0
  223. package/dist/persistence/SqlitePersistence.js.map +1 -0
  224. package/dist/pool/ConnectionPool.d.ts +215 -0
  225. package/dist/pool/ConnectionPool.d.ts.map +1 -0
  226. package/dist/pool/ConnectionPool.js +187 -0
  227. package/dist/pool/ConnectionPool.js.map +1 -0
  228. package/dist/registry/BaseRegistry.d.ts +203 -0
  229. package/dist/registry/BaseRegistry.d.ts.map +1 -0
  230. package/dist/registry/BaseRegistry.js +165 -0
  231. package/dist/registry/BaseRegistry.js.map +1 -0
  232. package/dist/registry/SkillRegistry.d.ts +69 -0
  233. package/dist/registry/SkillRegistry.d.ts.map +1 -0
  234. package/dist/registry/SkillRegistry.js +88 -0
  235. package/dist/registry/SkillRegistry.js.map +1 -0
  236. package/dist/registry/ToolRegistry.d.ts +69 -0
  237. package/dist/registry/ToolRegistry.d.ts.map +1 -0
  238. package/dist/registry/ToolRegistry.js +93 -0
  239. package/dist/registry/ToolRegistry.js.map +1 -0
  240. package/dist/sanitize.d.ts +63 -0
  241. package/dist/sanitize.d.ts.map +1 -0
  242. package/dist/sanitize.js +14 -0
  243. package/dist/sanitize.js.map +1 -0
  244. package/dist/schema.d.ts +531 -0
  245. package/dist/schema.d.ts.map +1 -0
  246. package/dist/schema.js +204 -0
  247. package/dist/schema.js.map +1 -0
  248. package/dist/telemetry/Telemetry.d.ts +36 -0
  249. package/dist/telemetry/Telemetry.d.ts.map +1 -0
  250. package/dist/telemetry/Telemetry.js +68 -0
  251. package/dist/telemetry/Telemetry.js.map +1 -0
  252. package/dist/telemetry/__tests__/Telemetry.test.d.ts +2 -0
  253. package/dist/telemetry/__tests__/Telemetry.test.d.ts.map +1 -0
  254. package/dist/transport/BaseTransport.d.ts +184 -0
  255. package/dist/transport/BaseTransport.d.ts.map +1 -0
  256. package/dist/transport/BaseTransport.js +200 -0
  257. package/dist/transport/BaseTransport.js.map +1 -0
  258. package/dist/transport/HttpHelpers.d.ts +60 -0
  259. package/dist/transport/HttpHelpers.d.ts.map +1 -0
  260. package/dist/transport/HttpHelpers.js +50 -0
  261. package/dist/transport/HttpHelpers.js.map +1 -0
  262. package/dist/transport/HttpTransport.d.ts +134 -0
  263. package/dist/transport/HttpTransport.d.ts.map +1 -0
  264. package/dist/transport/HttpTransport.js +175 -0
  265. package/dist/transport/HttpTransport.js.map +1 -0
  266. package/dist/transport/SseTransport.d.ts +133 -0
  267. package/dist/transport/SseTransport.d.ts.map +1 -0
  268. package/dist/transport/SseTransport.js +318 -0
  269. package/dist/transport/SseTransport.js.map +1 -0
  270. package/dist/transport/StreamableHttpTransport.d.ts +224 -0
  271. package/dist/transport/StreamableHttpTransport.d.ts.map +1 -0
  272. package/dist/transport/StreamableHttpTransport.js +407 -0
  273. package/dist/transport/StreamableHttpTransport.js.map +1 -0
  274. package/dist/types/disposable.d.ts +22 -0
  275. package/dist/types/disposable.d.ts.map +1 -0
  276. package/dist/types/disposable.js +1 -0
  277. package/dist/types/server-config.d.ts +32 -0
  278. package/dist/types/server-config.d.ts.map +1 -0
  279. package/dist/types/server-config.js +1 -0
  280. package/dist/types/skill.d.ts +69 -0
  281. package/dist/types/skill.d.ts.map +1 -0
  282. package/dist/types/skill.js +1 -0
  283. package/dist/types/tool.d.ts +68 -0
  284. package/dist/types/tool.d.ts.map +1 -0
  285. package/dist/types/tool.js +1 -0
  286. package/dist/watchers/SkillWatcher.d.ts +132 -0
  287. package/dist/watchers/SkillWatcher.d.ts.map +1 -0
  288. package/dist/watchers/SkillWatcher.js +73 -0
  289. package/dist/watchers/SkillWatcher.js.map +1 -0
  290. package/dist/watchers/ToolWatcher.d.ts +109 -0
  291. package/dist/watchers/ToolWatcher.d.ts.map +1 -0
  292. package/dist/watchers/ToolWatcher.js +71 -0
  293. package/dist/watchers/ToolWatcher.js.map +1 -0
  294. package/package.json +95 -0
@@ -0,0 +1,175 @@
1
+ import { createServer } from "node:http";
2
+ import { safeParse } from "valibot";
3
+ import { JsonRpcRequestSchema } from "../schema.js";
4
+ import { BaseTransport } from "./BaseTransport.js";
5
+ import { readRequestBody, sendCorsPreflight, sendJsonRpcError, sendJsonRpcResponse } from "./HttpHelpers.js";
6
+ class HttpTransport extends BaseTransport {
7
+ _server;
8
+ _mcpServer = null;
9
+ _requestTimeout;
10
+ _bodySizeLimitEnabled;
11
+ _maxBodySize;
12
+ _requestCount = 0;
13
+ _activeRequests = 0;
14
+ _path;
15
+ _metrics;
16
+ _metricsProvider;
17
+ constructor(options = {}){
18
+ super(options);
19
+ this._requestTimeout = options.requestTimeout ?? 30000;
20
+ this._bodySizeLimitEnabled = options.enableBodySizeLimit ?? true;
21
+ this._maxBodySize = options.maxBodySize ?? 10485760;
22
+ this._path = options.path ?? '/messages';
23
+ this._metrics = options.metrics;
24
+ this._metricsProvider = options.metricsProvider ?? null;
25
+ this._server = createServer((req, res)=>this._handleRequest(req, res));
26
+ }
27
+ get clientCount() {
28
+ return this._activeRequests;
29
+ }
30
+ async connect(mcpServer) {
31
+ this._mcpServer = mcpServer;
32
+ return new Promise((resolve)=>{
33
+ this._server.listen(this._port, this._host, ()=>{
34
+ this.log('info', `HTTP transport listening on http://${this._host}:${this._port}`);
35
+ resolve();
36
+ });
37
+ });
38
+ }
39
+ _trackError(errorType) {
40
+ this._metrics?.counter('http_request_errors_total', 1, {
41
+ transport: 'http',
42
+ error_type: errorType
43
+ }, 'Total HTTP request errors');
44
+ }
45
+ async _handleRequest(req, res) {
46
+ const startTime = Date.now();
47
+ const requestPath = req.url || '/';
48
+ const requestMethod = req.method || 'GET';
49
+ this._metrics?.counter('http_requests_total', 1, {}, 'Total HTTP transport requests');
50
+ this._metrics?.counter('http_transport_requests_total', 1, {
51
+ transport: 'http',
52
+ method: requestMethod,
53
+ path: requestPath
54
+ }, 'Total HTTP requests by transport');
55
+ res.once('finish', ()=>{
56
+ const durationSeconds = (Date.now() - startTime) / 1000;
57
+ this._metrics?.histogram('http_request_duration_seconds', durationSeconds, {});
58
+ this._metrics?.histogram('http_transport_request_duration_seconds', durationSeconds, {
59
+ transport: 'http',
60
+ path: requestPath
61
+ });
62
+ });
63
+ if (!this.validateHostHeader(req)) {
64
+ this._trackError('forbidden');
65
+ sendJsonRpcError(res, 403, -32000, 'Forbidden - invalid host header');
66
+ return;
67
+ }
68
+ if (this.isShuttingDown()) {
69
+ this._trackError('shutting_down');
70
+ sendJsonRpcError(res, 503, -32603, 'Server is shutting down');
71
+ return;
72
+ }
73
+ const clientIp = this.getClientIp(req);
74
+ if (this.checkRateLimit(clientIp)) {
75
+ this._trackError('rate_limit');
76
+ res.setHeader('Retry-After', '60');
77
+ sendJsonRpcError(res, 429, -32000, 'Too many requests');
78
+ return;
79
+ }
80
+ if (!this.validateCorsOrigin(req)) {
81
+ this._trackError('forbidden');
82
+ sendJsonRpcError(res, 403, -32000, 'Forbidden - invalid origin');
83
+ return;
84
+ }
85
+ this.setCorsHeaders(res);
86
+ if ('GET' === req.method && '/metrics' === req.url) return this.handleMetricsEndpoint(res, this._metricsProvider);
87
+ if ('OPTIONS' === req.method) return sendCorsPreflight(res);
88
+ if ('GET' === req.method && '/health' === req.url) return this.handleHealthEndpoint(res, {
89
+ requests: this._requestCount
90
+ });
91
+ if ('GET' === req.method && '/ready' === req.url) return this.handleReadinessEndpoint(res);
92
+ if ('POST' === req.method && req.url === this._path) return this._handlePostRequest(req, res);
93
+ this._trackError('not_found');
94
+ sendJsonRpcError(res, 404, -32601, 'Not Found');
95
+ }
96
+ async _handlePostRequest(req, res) {
97
+ this._requestCount++;
98
+ this._activeRequests++;
99
+ const timeout = setTimeout(()=>{
100
+ this._activeRequests--;
101
+ this._trackError('timeout');
102
+ sendJsonRpcError(res, 500, -32603, 'Request timeout');
103
+ }, this._requestTimeout);
104
+ try {
105
+ const maxBodySize = this._bodySizeLimitEnabled ? this._maxBodySize : 0;
106
+ const body = await readRequestBody(req, maxBodySize);
107
+ if (null === body) {
108
+ clearTimeout(timeout);
109
+ this._activeRequests--;
110
+ this._trackError('payload_too_large');
111
+ sendJsonRpcError(res, 413, -32000, 'Request body too large');
112
+ return;
113
+ }
114
+ let jsonRpcRequest;
115
+ try {
116
+ jsonRpcRequest = JSON.parse(body);
117
+ } catch {
118
+ clearTimeout(timeout);
119
+ this._activeRequests--;
120
+ this._trackError('parse_error');
121
+ sendJsonRpcError(res, 200, -32700, 'Parse error');
122
+ return;
123
+ }
124
+ const parseResult = safeParse(JsonRpcRequestSchema, jsonRpcRequest);
125
+ if (!parseResult.success) {
126
+ clearTimeout(timeout);
127
+ this._activeRequests--;
128
+ this._trackError('validation');
129
+ sendJsonRpcError(res, 200, -32600, 'Invalid Request', jsonRpcRequest?.id ?? null, parseResult.issues);
130
+ return;
131
+ }
132
+ if (!this._mcpServer) {
133
+ clearTimeout(timeout);
134
+ this._activeRequests--;
135
+ this._trackError('server_not_ready');
136
+ sendJsonRpcError(res, 200, -32603, 'Server not ready', jsonRpcRequest?.id ?? null);
137
+ return;
138
+ }
139
+ const response = await this._mcpServer.receive(jsonRpcRequest, {
140
+ sessionInfo: {}
141
+ });
142
+ clearTimeout(timeout);
143
+ this._activeRequests--;
144
+ if (response) sendJsonRpcResponse(res, response);
145
+ else {
146
+ res.writeHead(204);
147
+ res.end();
148
+ }
149
+ } catch (error) {
150
+ clearTimeout(timeout);
151
+ this._activeRequests--;
152
+ this._trackError('internal_error');
153
+ sendJsonRpcError(res, 200, -32603, 'Internal error', null, error instanceof Error ? error.message : String(error));
154
+ }
155
+ }
156
+ get requestCount() {
157
+ return this._requestCount;
158
+ }
159
+ async stop() {
160
+ this._isShuttingDown = true;
161
+ this._stopRateLimitCleanup();
162
+ return new Promise((resolve)=>{
163
+ this._server.close(()=>{
164
+ this.log('info', 'HTTP transport stopped');
165
+ resolve();
166
+ });
167
+ });
168
+ }
169
+ }
170
+ function createHttpTransport(options = {}) {
171
+ return new HttpTransport(options);
172
+ }
173
+ export { HttpTransport, createHttpTransport };
174
+
175
+ //# sourceMappingURL=HttpTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport/HttpTransport.js","sources":["../../src/transport/HttpTransport.ts"],"sourcesContent":["/**\n * HTTP Transport implementation.\n *\n * This transport provides a stateless, REST-like API interface for MCP tool invocations\n * using standard HTTP request-response patterns.\n *\n * @example\n * ```typescript\n * const transport = new HttpTransport({\n * port: 3000,\n * host: 'localhost'\n * });\n * await transport.connect(server);\n * ```\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'node:http';\nimport type { McpServer } from 'tmcp';\nimport { safeParse } from 'valibot';\nimport type { IMetrics } from '../contracts/index.js';\nimport { JsonRpcRequestSchema } from '../schema.js';\nimport { BaseTransport, type TransportOptions } from './BaseTransport.js';\nimport {\n\treadRequestBody,\n\tsendCorsPreflight,\n\tsendJsonRpcError,\n\tsendJsonRpcResponse,\n} from './HttpHelpers.js';\n\nexport interface HttpTransportOptions extends TransportOptions {\n\t/**\n\t * Path for messages endpoint\n\t * @default '/messages'\n\t */\n\tpath?: string;\n\tmetrics?: IMetrics;\n\tmetricsProvider?: () => string;\n\n\t/**\n\t * Enable request body size limit\n\t * @default true\n\t */\n\tenableBodySizeLimit?: boolean;\n\n\t/**\n\t * Maximum request body size in bytes\n\t * @default 10485760 (10MB)\n\t */\n\tmaxBodySize?: number;\n\n\t/**\n\t * Request timeout in milliseconds\n\t * @default 30000 (30 seconds)\n\t */\n\trequestTimeout?: number;\n}\n\n/**\n * HTTP Transport for MCP server.\n *\n * This transport provides a stateless, REST-like API interface for MCP tool invocations\n * using standard HTTP request-response patterns.\n *\n * @remarks\n * **Security Features:**\n * - Session ID validation (alphanumeric, max 64 chars)\n * - Query parameter sanitization (whitelist allowed keys)\n * - Rate limiting per IP (configurable, default 100 req/min)\n * - CORS origin validation\n * - Request body size limits (configurable, default 10MB)\n * - Request timeout (configurable, default 30s)\n *\n * **Rate Limiting:**\n * - Tracks requests per IP address within a time window\n * - Returns 429 Too Many Requests when limit exceeded\n * - Can be disabled via `enableRateLimit: false`\n *\n * **HTTP Status Code Mapping:**\n * - 200: Success (JSON-RPC response)\n * - 204: CORS Preflight (empty body)\n * - 400: Bad Request\n * - 403: Forbidden (invalid CORS)\n * - 404: Not Found\n * - 413: Payload Too Large\n * - 429: Too Many Requests\n * - 500: Internal Server Error\n * - 503: Server Not Ready\n */\nexport class HttpTransport extends BaseTransport {\n\tprivate _server: ReturnType<typeof createServer>;\n\tprivate _mcpServer: McpServer | null = null;\n\tprivate _requestTimeout: number;\n\tprivate _bodySizeLimitEnabled: boolean;\n\tprivate _maxBodySize: number;\n\tprivate _requestCount: number = 0;\n\tprivate _activeRequests: number = 0;\n\tprivate _path: string;\n\tprivate _metrics?: IMetrics;\n\tprivate _metricsProvider: (() => string) | null;\n\n\tconstructor(options: HttpTransportOptions = {}) {\n\t\tsuper(options);\n\n\t\tthis._requestTimeout = options.requestTimeout ?? 30000;\n\t\tthis._bodySizeLimitEnabled = options.enableBodySizeLimit ?? true;\n\t\tthis._maxBodySize = options.maxBodySize ?? 10 * 1024 * 1024;\n\t\tthis._path = options.path ?? '/messages';\n\t\tthis._metrics = options.metrics;\n\t\tthis._metricsProvider = options.metricsProvider ?? null;\n\t\tthis._server = createServer((req, res) => this._handleRequest(req, res));\n\t}\n\n\t/**\n\t * Get number of active HTTP connections.\n\t */\n\tget clientCount(): number {\n\t\treturn this._activeRequests;\n\t}\n\n\t/**\n\t * Connects MCP server to this transport.\n\t */\n\tasync connect(mcpServer: McpServer): Promise<void> {\n\t\tthis._mcpServer = mcpServer;\n\t\treturn new Promise((resolve) => {\n\t\t\tthis._server.listen(this._port, this._host, () => {\n\t\t\t\tthis.log('info', `HTTP transport listening on http://${this._host}:${this._port}`);\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Track an error in metrics.\n\t */\n\tprivate _trackError(errorType: string): void {\n\t\tthis._metrics?.counter(\n\t\t\t'http_request_errors_total',\n\t\t\t1,\n\t\t\t{ transport: 'http', error_type: errorType },\n\t\t\t'Total HTTP request errors'\n\t\t);\n\t}\n\n\t/**\n\t * Route and handle incoming HTTP requests.\n\t *\n\t * Performs security checks (host, shutdown, rate limit, CORS) then\n\t * dispatches to the appropriate endpoint handler.\n\t */\n\tprivate async _handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n\t\tconst startTime = Date.now();\n\t\tconst requestPath = req.url || '/';\n\t\tconst requestMethod = req.method || 'GET';\n\t\tthis._metrics?.counter('http_requests_total', 1, {}, 'Total HTTP transport requests');\n\t\tthis._metrics?.counter(\n\t\t\t'http_transport_requests_total',\n\t\t\t1,\n\t\t\t{ transport: 'http', method: requestMethod, path: requestPath },\n\t\t\t'Total HTTP requests by transport'\n\t\t);\n\t\tres.once('finish', () => {\n\t\t\tconst durationSeconds = (Date.now() - startTime) / 1000;\n\t\t\tthis._metrics?.histogram('http_request_duration_seconds', durationSeconds, {});\n\t\t\tthis._metrics?.histogram('http_transport_request_duration_seconds', durationSeconds, {\n\t\t\t\ttransport: 'http',\n\t\t\t\tpath: requestPath,\n\t\t\t});\n\t\t});\n\n\t\t// Security middleware chain\n\t\tif (!this.validateHostHeader(req)) {\n\t\t\tthis._trackError('forbidden');\n\t\t\tsendJsonRpcError(res, 403, -32000, 'Forbidden - invalid host header');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.isShuttingDown()) {\n\t\t\tthis._trackError('shutting_down');\n\t\t\tsendJsonRpcError(res, 503, -32603, 'Server is shutting down');\n\t\t\treturn;\n\t\t}\n\n\t\tconst clientIp = this.getClientIp(req);\n\t\tif (this.checkRateLimit(clientIp)) {\n\t\t\tthis._trackError('rate_limit');\n\t\t\tres.setHeader('Retry-After', '60');\n\t\t\tsendJsonRpcError(res, 429, -32000, 'Too many requests');\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.validateCorsOrigin(req)) {\n\t\t\tthis._trackError('forbidden');\n\t\t\tsendJsonRpcError(res, 403, -32000, 'Forbidden - invalid origin');\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setCorsHeaders(res);\n\n\t\t// Static endpoints\n\t\tif (req.method === 'GET' && req.url === '/metrics')\n\t\t\treturn this.handleMetricsEndpoint(res, this._metricsProvider);\n\t\tif (req.method === 'OPTIONS') return sendCorsPreflight(res);\n\t\tif (req.method === 'GET' && req.url === '/health')\n\t\t\treturn this.handleHealthEndpoint(res, { requests: this._requestCount });\n\t\tif (req.method === 'GET' && req.url === '/ready') return this.handleReadinessEndpoint(res);\n\n\t\t// MCP endpoint\n\t\tif (req.method === 'POST' && req.url === this._path) return this._handlePostRequest(req, res);\n\n\t\t// 404\n\t\tthis._trackError('not_found');\n\t\tsendJsonRpcError(res, 404, -32601, 'Not Found');\n\t}\n\n\t/**\n\t * Handle POST to the MCP messages endpoint.\n\t *\n\t * Reads the request body, validates JSON-RPC format, and delegates\n\t * processing to the MCP server.\n\t */\n\tprivate async _handlePostRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n\t\tthis._requestCount++;\n\t\tthis._activeRequests++;\n\n\t\tconst timeout = setTimeout(() => {\n\t\t\tthis._activeRequests--;\n\t\t\tthis._trackError('timeout');\n\t\t\tsendJsonRpcError(res, 500, -32603, 'Request timeout');\n\t\t}, this._requestTimeout);\n\n\t\ttry {\n\t\t\tconst maxBodySize = this._bodySizeLimitEnabled ? this._maxBodySize : 0;\n\t\t\tconst body = await readRequestBody(req, maxBodySize);\n\n\t\t\tif (body === null) {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tthis._activeRequests--;\n\t\t\t\tthis._trackError('payload_too_large');\n\t\t\t\tsendJsonRpcError(res, 413, -32000, 'Request body too large');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet jsonRpcRequest;\n\t\t\ttry {\n\t\t\t\tjsonRpcRequest = JSON.parse(body);\n\t\t\t} catch {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tthis._activeRequests--;\n\t\t\t\tthis._trackError('parse_error');\n\t\t\t\tsendJsonRpcError(res, 200, -32700, 'Parse error');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst parseResult = safeParse(JsonRpcRequestSchema, jsonRpcRequest);\n\t\t\tif (!parseResult.success) {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tthis._activeRequests--;\n\t\t\t\tthis._trackError('validation');\n\t\t\t\tsendJsonRpcError(\n\t\t\t\t\tres,\n\t\t\t\t\t200,\n\t\t\t\t\t-32600,\n\t\t\t\t\t'Invalid Request',\n\t\t\t\t\tjsonRpcRequest?.id ?? null,\n\t\t\t\t\tparseResult.issues\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!this._mcpServer) {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tthis._activeRequests--;\n\t\t\t\tthis._trackError('server_not_ready');\n\t\t\t\tsendJsonRpcError(res, 200, -32603, 'Server not ready', jsonRpcRequest?.id ?? null);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst response = await this._mcpServer.receive(jsonRpcRequest, {\n\t\t\t\tsessionInfo: {},\n\t\t\t});\n\n\t\t\tclearTimeout(timeout);\n\t\t\tthis._activeRequests--;\n\n\t\t\tif (response) {\n\t\t\t\tsendJsonRpcResponse(res, response);\n\t\t\t} else {\n\t\t\t\tres.writeHead(204);\n\t\t\t\tres.end();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tclearTimeout(timeout);\n\t\t\tthis._activeRequests--;\n\t\t\tthis._trackError('internal_error');\n\t\t\tsendJsonRpcError(\n\t\t\t\tres,\n\t\t\t\t200,\n\t\t\t\t-32603,\n\t\t\t\t'Internal error',\n\t\t\t\tnull,\n\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Returns number of requests handled.\n\t */\n\tget requestCount(): number {\n\t\treturn this._requestCount;\n\t}\n\n\t/**\n\t * Stops transport server.\n\t */\n\tasync stop(): Promise<void> {\n\t\tthis._isShuttingDown = true;\n\t\tthis._stopRateLimitCleanup();\n\n\t\treturn new Promise((resolve) => {\n\t\t\tthis._server.close(() => {\n\t\t\t\tthis.log('info', 'HTTP transport stopped');\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\t}\n}\n\n/**\n * Create an HTTP transport with given options.\n *\n * @param options - Transport configuration\n * @returns A configured HTTP transport\n *\n * @example\n * ```typescript\n * const transport = new HttpTransport({ port: 3000 });\n * await transport.connect(server);\n * ```\n */\nexport function createHttpTransport(options: HttpTransportOptions = {}): HttpTransport {\n\treturn new HttpTransport(options);\n}\n"],"names":["HttpTransport","BaseTransport","options","createServer","req","res","mcpServer","Promise","resolve","errorType","startTime","Date","requestPath","requestMethod","durationSeconds","sendJsonRpcError","clientIp","sendCorsPreflight","timeout","setTimeout","maxBodySize","body","readRequestBody","clearTimeout","jsonRpcRequest","JSON","parseResult","safeParse","JsonRpcRequestSchema","response","sendJsonRpcResponse","error","Error","String","createHttpTransport"],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * SSE (Server-Sent Events) Transport implementation.
3
+ *
4
+ * This transport allows multiple concurrent connections over HTTP using Server-Sent Events,
5
+ * enabling multi-user scenarios and horizontal scaling.
6
+ *
7
+ * When a ConnectionPool is provided, each SSE client gets an isolated session with its own
8
+ * thought history. Without a pool, all clients share a single server instance (backward compatible).
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const transport = new SseTransport({
13
+ * port: 3000,
14
+ * host: 'localhost'
15
+ * });
16
+ * await transport.connect(server);
17
+ * ```
18
+ */
19
+ import type { McpServer } from 'tmcp';
20
+ import type { IMetrics } from '../contracts/index.js';
21
+ import type { ConnectionPool } from '../pool/ConnectionPool.js';
22
+ import { BaseTransport, type TransportOptions } from './BaseTransport.js';
23
+ /**
24
+ * SSE-specific transport options extending base TransportOptions.
25
+ */
26
+ export interface SseTransportOptions extends TransportOptions {
27
+ path?: string;
28
+ metrics?: IMetrics;
29
+ /**
30
+ * Optional connection pool for per-session state isolation.
31
+ * When provided, each SSE client gets an isolated thought history.
32
+ * When omitted, all clients share a single server instance (backward compatible).
33
+ */
34
+ connectionPool?: ConnectionPool;
35
+ }
36
+ /**
37
+ * SSE Transport for MCP server over HTTP.
38
+ *
39
+ * This transport uses Server-Sent Events (SSE) to communicate with clients,
40
+ * allowing multiple concurrent connections and web-based clients.
41
+ *
42
+ * @remarks
43
+ * **Security Features:**
44
+ * - Session ID validation (alphanumeric, max 64 chars)
45
+ * - Query parameter sanitization (whitelist allowed keys)
46
+ * - Rate limiting per IP (configurable, default 100 req/min)
47
+ * - CORS origin validation
48
+ *
49
+ * **Rate Limiting:**
50
+ * - Tracks requests per IP address within a time window
51
+ * - Returns 429 Too Many Requests when limit exceeded
52
+ * - Can be disabled via `enableRateLimit: false`
53
+ */
54
+ export declare class SseTransport extends BaseTransport {
55
+ private _server;
56
+ private _path;
57
+ private _clients;
58
+ private _clientSessionMap;
59
+ private _messageQueue;
60
+ private _metrics?;
61
+ private _connectionPool?;
62
+ constructor(options?: SseTransportOptions);
63
+ /**
64
+ * Connect MCP server to this transport.
65
+ *
66
+ * @param mcpServer - The MCP server instance
67
+ */
68
+ connect(mcpServer: McpServer): Promise<void>;
69
+ private _mcpServer;
70
+ /**
71
+ * Handle incoming HTTP requests
72
+ */
73
+ private _handleRequest;
74
+ /**
75
+ * Handle health check (liveness) endpoint
76
+ */
77
+ private _handleHealthCheck;
78
+ /**
79
+ * Handle readiness check endpoint
80
+ */
81
+ private _handleReadinessCheck;
82
+ /**
83
+ * Handle new SSE connection
84
+ */
85
+ private _handleSseConnection;
86
+ /**
87
+ * Handle incoming message from client
88
+ */
89
+ private _handleMessage;
90
+ /**
91
+ * Send an SSE event to a specific client
92
+ */
93
+ private _sendSseEvent;
94
+ private _updateActiveConnectionsMetric;
95
+ private _updatePoolMetrics;
96
+ /**
97
+ * Broadcast a message to all connected clients
98
+ */
99
+ broadcast(event: string, data: unknown): void;
100
+ /**
101
+ * Generate a unique client ID
102
+ */
103
+ private _generateClientId;
104
+ /**
105
+ * Get number of connected clients
106
+ */
107
+ get clientCount(): number;
108
+ /**
109
+ * Get the connection pool, if one was configured.
110
+ */
111
+ get connectionPool(): ConnectionPool | undefined;
112
+ /**
113
+ * Stop the transport server with graceful shutdown.
114
+ *
115
+ * @param timeout - Maximum time to wait for requests to drain (not used for SSE)
116
+ * @returns Promise that resolves when shutdown is complete
117
+ */
118
+ stop(_timeout?: number): Promise<void>;
119
+ }
120
+ /**
121
+ * Create an SSE transport with given options.
122
+ *
123
+ * @param options - Transport configuration
124
+ * @returns A configured SSE transport
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const transport = createSseTransport({ port: 3000 });
129
+ * await transport.connect(mcpServer);
130
+ * ```
131
+ */
132
+ export declare function createSseTransport(options?: SseTransportOptions): SseTransport;
133
+ //# sourceMappingURL=SseTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SseTransport.d.ts","sourceRoot":"","sources":["../../src/transport/SseTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE,OAAO,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC1E;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,gBAAgB;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,YAAa,SAAQ,aAAa;IAC9C,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,iBAAiB,CAA0C;IACnE,OAAO,CAAC,aAAa,CAAqC;IAC1D,OAAO,CAAC,QAAQ,CAAC,CAAW;IAC5B,OAAO,CAAC,eAAe,CAAC,CAAiB;gBAE7B,OAAO,GAAE,mBAAwB;IAU7C;;;;OAIG;IACG,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD,OAAO,CAAC,UAAU,CAA0B;IAE5C;;OAEG;YACW,cAAc;IA2F5B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAc1B;;OAEG;YACW,qBAAqB;IAYnC;;OAEG;YACW,oBAAoB;IA6DlC;;OAEG;YACW,cAAc;IAyD5B;;OAEG;IACH,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,8BAA8B;IAStC,OAAO,CAAC,kBAAkB;IAyB1B;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IAM7C;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;OAEG;IACH,IAAI,cAAc,IAAI,cAAc,GAAG,SAAS,CAE/C;IAED;;;;;OAKG;IACG,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA6B5C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,CAElF"}
@@ -0,0 +1,318 @@
1
+ import { createServer } from "node:http";
2
+ import { URL } from "node:url";
3
+ import { safeParse } from "valibot";
4
+ import { JsonRpcRequestSchema } from "../schema.js";
5
+ import { BaseTransport } from "./BaseTransport.js";
6
+ class SseTransport extends BaseTransport {
7
+ _server;
8
+ _path;
9
+ _clients = new Set();
10
+ _clientSessionMap = new Map();
11
+ _messageQueue = new Map();
12
+ _metrics;
13
+ _connectionPool;
14
+ constructor(options = {}){
15
+ super(options);
16
+ this._path = options.path ?? '/sse';
17
+ this._metrics = options.metrics;
18
+ this._connectionPool = options.connectionPool;
19
+ this._updateActiveConnectionsMetric();
20
+ this._server = createServer((req, res)=>this._handleRequest(req, res));
21
+ }
22
+ async connect(mcpServer) {
23
+ this._mcpServer = mcpServer;
24
+ return new Promise((resolve)=>{
25
+ this._server.listen(this._port, this._host, ()=>{
26
+ this.log('info', `SSE transport listening on http://${this._host}:${this._port}`);
27
+ resolve();
28
+ });
29
+ });
30
+ }
31
+ _mcpServer = null;
32
+ async _handleRequest(req, res) {
33
+ const startTime = Date.now();
34
+ const requestPath = req.url || '/';
35
+ const requestMethod = req.method || 'GET';
36
+ this._metrics?.counter('http_requests_total', 1, {
37
+ transport: 'sse',
38
+ method: requestMethod,
39
+ path: requestPath
40
+ }, 'Total HTTP requests');
41
+ res.once('finish', ()=>{
42
+ const durationSeconds = (Date.now() - startTime) / 1000;
43
+ this._metrics?.histogram('http_request_duration_seconds', durationSeconds, {
44
+ transport: 'sse',
45
+ path: requestPath
46
+ });
47
+ });
48
+ if (!this.validateHostHeader(req)) {
49
+ this._metrics?.counter('http_request_errors_total', 1, {
50
+ transport: 'sse',
51
+ error_type: 'forbidden'
52
+ }, 'Total HTTP request errors');
53
+ res.writeHead(403, {
54
+ 'Content-Type': 'application/json'
55
+ });
56
+ res.end(JSON.stringify({
57
+ error: 'Forbidden - invalid host header'
58
+ }));
59
+ return;
60
+ }
61
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
62
+ const clientIp = this.getClientIp(req);
63
+ if (this.checkRateLimit(clientIp)) {
64
+ this._metrics?.counter('http_request_errors_total', 1, {
65
+ transport: 'sse',
66
+ error_type: 'rate_limit'
67
+ }, 'Total HTTP request errors');
68
+ res.writeHead(429, {
69
+ 'Content-Type': 'application/json',
70
+ 'Retry-After': '60'
71
+ });
72
+ res.end(JSON.stringify({
73
+ error: 'Too many requests'
74
+ }));
75
+ return;
76
+ }
77
+ if (!this.validateCorsOrigin(req)) {
78
+ this._metrics?.counter('http_request_errors_total', 1, {
79
+ transport: 'sse',
80
+ error_type: 'forbidden'
81
+ }, 'Total HTTP request errors');
82
+ res.writeHead(403, {
83
+ 'Content-Type': 'application/json'
84
+ });
85
+ res.end(JSON.stringify({
86
+ error: 'Forbidden - invalid origin'
87
+ }));
88
+ return;
89
+ }
90
+ this.setCorsHeaders(res);
91
+ const sanitizedParams = this.sanitizeQueryParams(url);
92
+ if (sanitizedParams.session || sanitizedParams.sessionId) {
93
+ const sessionId = sanitizedParams.session ?? sanitizedParams.sessionId;
94
+ if (!this.validateSessionId(sessionId)) {
95
+ this._metrics?.counter('http_request_errors_total', 1, {
96
+ transport: 'sse',
97
+ error_type: 'validation'
98
+ }, 'Total HTTP request errors');
99
+ res.writeHead(400, {
100
+ 'Content-Type': 'application/json'
101
+ });
102
+ res.end(JSON.stringify({
103
+ error: 'Invalid session ID format'
104
+ }));
105
+ return;
106
+ }
107
+ }
108
+ if (this._enableCors && 'OPTIONS' === req.method) {
109
+ res.writeHead(204);
110
+ res.end();
111
+ return;
112
+ }
113
+ if (url.pathname === this._path && 'GET' === req.method) return void await this._handleSseConnection(req, res, sanitizedParams);
114
+ if (url.pathname === `${this._path}/message` && 'POST' === req.method) return void await this._handleMessage(req, res, sanitizedParams);
115
+ if ('/health' === url.pathname) return void this._handleHealthCheck(res);
116
+ if ('/ready' === url.pathname) return void await this._handleReadinessCheck(res);
117
+ res.writeHead(404, {
118
+ 'Content-Type': 'text/plain'
119
+ });
120
+ res.end('Not Found');
121
+ }
122
+ _handleHealthCheck(res) {
123
+ const healthData = {
124
+ status: 'healthy',
125
+ clients: this._clients.size
126
+ };
127
+ if (this._connectionPool) {
128
+ const poolStats = this._connectionPool.getStats();
129
+ healthData.pool = poolStats;
130
+ }
131
+ if (this._healthChecker) {
132
+ const liveness = this._healthChecker.checkLiveness();
133
+ healthData.liveness = liveness;
134
+ }
135
+ res.writeHead(200, {
136
+ 'Content-Type': 'application/json'
137
+ });
138
+ res.end(JSON.stringify(healthData));
139
+ }
140
+ async _handleReadinessCheck(res) {
141
+ if (this._healthChecker) {
142
+ const readiness = await this._healthChecker.checkReadiness();
143
+ const statusCode = 'ok' === readiness.status ? 200 : 503;
144
+ res.writeHead(statusCode, {
145
+ 'Content-Type': 'application/json'
146
+ });
147
+ res.end(JSON.stringify(readiness));
148
+ } else {
149
+ res.writeHead(200, {
150
+ 'Content-Type': 'application/json'
151
+ });
152
+ res.end(JSON.stringify({
153
+ status: 'ok',
154
+ timestamp: new Date().toISOString(),
155
+ components: {}
156
+ }));
157
+ }
158
+ }
159
+ async _handleSseConnection(req, res, params) {
160
+ res.writeHead(200, {
161
+ 'Content-Type': 'text/event-stream',
162
+ 'Cache-Control': 'no-cache',
163
+ Connection: 'keep-alive'
164
+ });
165
+ let sessionId;
166
+ if (this._connectionPool) {
167
+ const requestedSession = params.session ?? params.sessionId;
168
+ if (requestedSession && this._connectionPool.getSessionInfo(requestedSession)) sessionId = requestedSession;
169
+ else try {
170
+ sessionId = await this._connectionPool.createSession();
171
+ } catch (error) {
172
+ res.write(`event: error\n`);
173
+ res.write(`data: ${JSON.stringify({
174
+ error: error instanceof Error ? error.message : 'Failed to create session'
175
+ })}\n\n`);
176
+ res.end();
177
+ return;
178
+ }
179
+ this._clientSessionMap.set(res, sessionId);
180
+ this._updatePoolMetrics();
181
+ }
182
+ const connectedPayload = {
183
+ timestamp: Date.now()
184
+ };
185
+ if (sessionId) connectedPayload.sessionId = sessionId;
186
+ this._sendSseEvent(res, 'connected', connectedPayload);
187
+ this._clients.add(res);
188
+ this._updateActiveConnectionsMetric();
189
+ req.on('close', ()=>{
190
+ this._clients.delete(res);
191
+ this._clientSessionMap.delete(res);
192
+ this._updateActiveConnectionsMetric();
193
+ });
194
+ const clientId = this._generateClientId();
195
+ const queued = this._messageQueue.get(clientId);
196
+ if (queued) {
197
+ for (const message of queued)this._sendSseEvent(res, 'message', message);
198
+ this._messageQueue.delete(clientId);
199
+ }
200
+ }
201
+ async _handleMessage(req, res, _params) {
202
+ let body = '';
203
+ for await (const chunk of req)body += chunk.toString();
204
+ try {
205
+ const jsonRpcRequest = JSON.parse(body);
206
+ const parseResult = safeParse(JsonRpcRequestSchema, jsonRpcRequest);
207
+ if (!parseResult.success) {
208
+ this._metrics?.counter('http_request_errors_total', 1, {
209
+ transport: 'sse',
210
+ error_type: 'validation'
211
+ }, 'Total HTTP request errors');
212
+ res.writeHead(200, {
213
+ 'Content-Type': 'application/json'
214
+ });
215
+ res.end(JSON.stringify({
216
+ jsonrpc: '2.0',
217
+ id: jsonRpcRequest?.id ?? null,
218
+ error: {
219
+ code: -32600,
220
+ message: 'Invalid Request',
221
+ data: parseResult.issues
222
+ }
223
+ }));
224
+ return;
225
+ }
226
+ if (this._mcpServer) {
227
+ const response = await this._mcpServer.receive(jsonRpcRequest, {
228
+ sessionInfo: {}
229
+ });
230
+ res.writeHead(200, {
231
+ 'Content-Type': 'application/json'
232
+ });
233
+ if (response) res.end(JSON.stringify(response));
234
+ else res.end(JSON.stringify({
235
+ jsonrpc: '2.0',
236
+ id: jsonRpcRequest?.id ?? null,
237
+ result: null
238
+ }));
239
+ } else {
240
+ this._metrics?.counter('http_request_errors_total', 1, {
241
+ transport: 'sse',
242
+ error_type: 'server_not_ready'
243
+ }, 'Total HTTP request errors');
244
+ res.writeHead(503, {
245
+ 'Content-Type': 'application/json'
246
+ });
247
+ res.end(JSON.stringify({
248
+ error: 'Server not ready'
249
+ }));
250
+ }
251
+ } catch {
252
+ this._metrics?.counter('http_request_errors_total', 1, {
253
+ transport: 'sse',
254
+ error_type: 'parse_error'
255
+ }, 'Total HTTP request errors');
256
+ res.writeHead(400, {
257
+ 'Content-Type': 'application/json'
258
+ });
259
+ res.end(JSON.stringify({
260
+ error: 'Invalid JSON'
261
+ }));
262
+ }
263
+ }
264
+ _sendSseEvent(res, event, data) {
265
+ try {
266
+ res.write(`event: ${event}\n`);
267
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
268
+ } catch {
269
+ this._clients.delete(res);
270
+ this._updateActiveConnectionsMetric();
271
+ }
272
+ }
273
+ _updateActiveConnectionsMetric() {
274
+ this._metrics?.gauge('sse_active_connections', this._clients.size, {}, 'Current active SSE connections');
275
+ }
276
+ _updatePoolMetrics() {
277
+ if (!this._connectionPool || !this._metrics) return;
278
+ const stats = this._connectionPool.getStats();
279
+ this._metrics.gauge('sse_pool_active_sessions', stats.activeSessions, {}, 'Active sessions in connection pool');
280
+ this._metrics.gauge('sse_pool_total_sessions', stats.totalSessions, {}, 'Total sessions in connection pool');
281
+ this._metrics.gauge('sse_pool_max_sessions', stats.maxSessions, {}, 'Maximum sessions in connection pool');
282
+ }
283
+ broadcast(event, data) {
284
+ for (const client of this._clients)this._sendSseEvent(client, event, data);
285
+ }
286
+ _generateClientId() {
287
+ return `client_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
288
+ }
289
+ get clientCount() {
290
+ return this._clients.size;
291
+ }
292
+ get connectionPool() {
293
+ return this._connectionPool;
294
+ }
295
+ async stop(_timeout) {
296
+ this._isShuttingDown = true;
297
+ this._stopRateLimitCleanup();
298
+ if (this._connectionPool) await this._connectionPool.terminate();
299
+ return new Promise((resolve)=>{
300
+ for (const client of this._clients)try {
301
+ client.end();
302
+ } catch {}
303
+ this._clients.clear();
304
+ this._clientSessionMap.clear();
305
+ this._updateActiveConnectionsMetric();
306
+ this._server.close(()=>{
307
+ this.log('info', 'SSE transport stopped');
308
+ resolve();
309
+ });
310
+ });
311
+ }
312
+ }
313
+ function createSseTransport(options = {}) {
314
+ return new SseTransport(options);
315
+ }
316
+ export { SseTransport, createSseTransport };
317
+
318
+ //# sourceMappingURL=SseTransport.js.map