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.
- package/LICENSE +24 -0
- package/README.md +112 -0
- package/dist/ServerConfig.d.ts +229 -0
- package/dist/ServerConfig.d.ts.map +1 -0
- package/dist/ServerConfig.js +121 -0
- package/dist/ServerConfig.js.map +1 -0
- package/dist/__tests__/base-registry.test.d.ts +2 -0
- package/dist/__tests__/base-registry.test.d.ts.map +1 -0
- package/dist/__tests__/base-transport-cov.test.d.ts +2 -0
- package/dist/__tests__/base-transport-cov.test.d.ts.map +1 -0
- package/dist/__tests__/base-transport.test.d.ts +2 -0
- package/dist/__tests__/base-transport.test.d.ts.map +1 -0
- package/dist/__tests__/config-loader.test.d.ts +2 -0
- package/dist/__tests__/config-loader.test.d.ts.map +1 -0
- package/dist/__tests__/connection-pool-cov.test.d.ts +2 -0
- package/dist/__tests__/connection-pool-cov.test.d.ts.map +1 -0
- package/dist/__tests__/connection-pool.test.d.ts +2 -0
- package/dist/__tests__/connection-pool.test.d.ts.map +1 -0
- package/dist/__tests__/container.test.d.ts +2 -0
- package/dist/__tests__/container.test.d.ts.map +1 -0
- package/dist/__tests__/crud.test.d.ts +2 -0
- package/dist/__tests__/crud.test.d.ts.map +1 -0
- package/dist/__tests__/discovery-cache.test.d.ts +2 -0
- package/dist/__tests__/discovery-cache.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/factories.test.d.ts +2 -0
- package/dist/__tests__/factories.test.d.ts.map +1 -0
- package/dist/__tests__/health-checker-cov.test.d.ts +2 -0
- package/dist/__tests__/health-checker-cov.test.d.ts.map +1 -0
- package/dist/__tests__/health-checker.test.d.ts +2 -0
- package/dist/__tests__/health-checker.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/factories.d.ts +36 -0
- package/dist/__tests__/helpers/factories.d.ts.map +1 -0
- package/dist/__tests__/helpers/index.d.ts +3 -0
- package/dist/__tests__/helpers/index.d.ts.map +1 -0
- package/dist/__tests__/helpers/timers.d.ts +4 -0
- package/dist/__tests__/helpers/timers.d.ts.map +1 -0
- package/dist/__tests__/history-manager.test.d.ts +2 -0
- package/dist/__tests__/history-manager.test.d.ts.map +1 -0
- package/dist/__tests__/http-helpers-cov.test.d.ts +2 -0
- package/dist/__tests__/http-helpers-cov.test.d.ts.map +1 -0
- package/dist/__tests__/http-transport-cov.test.d.ts +2 -0
- package/dist/__tests__/http-transport-cov.test.d.ts.map +1 -0
- package/dist/__tests__/http-transport.test.d.ts +2 -0
- package/dist/__tests__/http-transport.test.d.ts.map +1 -0
- package/dist/__tests__/input-normalizer.test.d.ts +8 -0
- package/dist/__tests__/input-normalizer.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/lib-server.test.d.ts +2 -0
- package/dist/__tests__/lib-server.test.d.ts.map +1 -0
- package/dist/__tests__/memory-persistence.test.d.ts +2 -0
- package/dist/__tests__/memory-persistence.test.d.ts.map +1 -0
- package/dist/__tests__/metrics-integration.test.d.ts +2 -0
- package/dist/__tests__/metrics-integration.test.d.ts.map +1 -0
- package/dist/__tests__/persistence.test.d.ts +2 -0
- package/dist/__tests__/persistence.test.d.ts.map +1 -0
- package/dist/__tests__/reasoning-integration.test.d.ts +11 -0
- package/dist/__tests__/reasoning-integration.test.d.ts.map +1 -0
- package/dist/__tests__/reasoning-types.test.d.ts +2 -0
- package/dist/__tests__/reasoning-types.test.d.ts.map +1 -0
- package/dist/__tests__/request-context.test.d.ts +2 -0
- package/dist/__tests__/request-context.test.d.ts.map +1 -0
- package/dist/__tests__/sanitize.test.d.ts +2 -0
- package/dist/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/__tests__/schema.test.d.ts +2 -0
- package/dist/__tests__/schema.test.d.ts.map +1 -0
- package/dist/__tests__/sequentialthinking-tools.test.d.ts +2 -0
- package/dist/__tests__/sequentialthinking-tools.test.d.ts.map +1 -0
- package/dist/__tests__/server-config.test.d.ts +2 -0
- package/dist/__tests__/server-config.test.d.ts.map +1 -0
- package/dist/__tests__/skill-discovery.test.d.ts +2 -0
- package/dist/__tests__/skill-discovery.test.d.ts.map +1 -0
- package/dist/__tests__/skill-registry.test.d.ts +2 -0
- package/dist/__tests__/skill-registry.test.d.ts.map +1 -0
- package/dist/__tests__/skill-watcher.test.d.ts +2 -0
- package/dist/__tests__/skill-watcher.test.d.ts.map +1 -0
- package/dist/__tests__/sqlite-persistence.test.d.ts +2 -0
- package/dist/__tests__/sqlite-persistence.test.d.ts.map +1 -0
- package/dist/__tests__/sse-transport-cov.test.d.ts +2 -0
- package/dist/__tests__/sse-transport-cov.test.d.ts.map +1 -0
- package/dist/__tests__/sse-transport.test.d.ts +2 -0
- package/dist/__tests__/sse-transport.test.d.ts.map +1 -0
- package/dist/__tests__/streamable-http-cov.test.d.ts +2 -0
- package/dist/__tests__/streamable-http-cov.test.d.ts.map +1 -0
- package/dist/__tests__/streamable-http-transport.test.d.ts +2 -0
- package/dist/__tests__/streamable-http-transport.test.d.ts.map +1 -0
- package/dist/__tests__/structured-logger.test.d.ts +2 -0
- package/dist/__tests__/structured-logger.test.d.ts.map +1 -0
- package/dist/__tests__/thought-evaluator.test.d.ts +2 -0
- package/dist/__tests__/thought-evaluator.test.d.ts.map +1 -0
- package/dist/__tests__/thought-formatter.test.d.ts +2 -0
- package/dist/__tests__/thought-formatter.test.d.ts.map +1 -0
- package/dist/__tests__/thought-processor.test.d.ts +8 -0
- package/dist/__tests__/thought-processor.test.d.ts.map +1 -0
- package/dist/__tests__/tool-registry-cov.test.d.ts +2 -0
- package/dist/__tests__/tool-registry-cov.test.d.ts.map +1 -0
- package/dist/__tests__/tool-registry.test.d.ts +2 -0
- package/dist/__tests__/tool-registry.test.d.ts.map +1 -0
- package/dist/__tests__/tool-watcher.test.d.ts +2 -0
- package/dist/__tests__/tool-watcher.test.d.ts.map +1 -0
- package/dist/__tests__/worker-manager-cov.test.d.ts +2 -0
- package/dist/__tests__/worker-manager-cov.test.d.ts.map +1 -0
- package/dist/__tests__/worker-manager.test.d.ts +2 -0
- package/dist/__tests__/worker-manager.test.d.ts.map +1 -0
- package/dist/cache/DiscoveryCache.d.ts +269 -0
- package/dist/cache/DiscoveryCache.d.ts.map +1 -0
- package/dist/cache/DiscoveryCache.js +100 -0
- package/dist/cache/DiscoveryCache.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +114 -0
- package/dist/cli.js.map +1 -0
- package/dist/cluster/WorkerManager.d.ts +166 -0
- package/dist/cluster/WorkerManager.d.ts.map +1 -0
- package/dist/cluster/WorkerManager.js +202 -0
- package/dist/cluster/WorkerManager.js.map +1 -0
- package/dist/cluster/worker.d.ts +11 -0
- package/dist/cluster/worker.d.ts.map +1 -0
- package/dist/cluster/worker.js +36 -0
- package/dist/cluster/worker.js.map +1 -0
- package/dist/config/ConfigLoader.d.ts +224 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +85 -0
- package/dist/config/ConfigLoader.js.map +1 -0
- package/dist/context/RequestContext.d.ts +61 -0
- package/dist/context/RequestContext.d.ts.map +1 -0
- package/dist/context/RequestContext.js +17 -0
- package/dist/context/RequestContext.js.map +1 -0
- package/dist/contracts/index.d.ts +10 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/interfaces.d.ts +107 -0
- package/dist/contracts/interfaces.d.ts.map +1 -0
- package/dist/contracts/interfaces.js +1 -0
- package/dist/core/HistoryManager.d.ts +514 -0
- package/dist/core/HistoryManager.d.ts.map +1 -0
- package/dist/core/HistoryManager.js +331 -0
- package/dist/core/HistoryManager.js.map +1 -0
- package/dist/core/IHistoryManager.d.ts +100 -0
- package/dist/core/IHistoryManager.d.ts.map +1 -0
- package/dist/core/IHistoryManager.js +1 -0
- package/dist/core/InputNormalizer.d.ts +139 -0
- package/dist/core/InputNormalizer.d.ts.map +1 -0
- package/dist/core/InputNormalizer.js +101 -0
- package/dist/core/InputNormalizer.js.map +1 -0
- package/dist/core/ThoughtEvaluator.d.ts +127 -0
- package/dist/core/ThoughtEvaluator.d.ts.map +1 -0
- package/dist/core/ThoughtEvaluator.js +346 -0
- package/dist/core/ThoughtEvaluator.js.map +1 -0
- package/dist/core/ThoughtFormatter.d.ts +133 -0
- package/dist/core/ThoughtFormatter.d.ts.map +1 -0
- package/dist/core/ThoughtFormatter.js +70 -0
- package/dist/core/ThoughtFormatter.js.map +1 -0
- package/dist/core/ThoughtProcessor.d.ts +218 -0
- package/dist/core/ThoughtProcessor.d.ts.map +1 -0
- package/dist/core/ThoughtProcessor.js +205 -0
- package/dist/core/ThoughtProcessor.js.map +1 -0
- package/dist/core/reasoning.d.ts +169 -0
- package/dist/core/reasoning.d.ts.map +1 -0
- package/dist/core/reasoning.js +1 -0
- package/dist/core/step.d.ts +45 -0
- package/dist/core/step.d.ts.map +1 -0
- package/dist/core/step.js +1 -0
- package/dist/core/thought.d.ts +190 -0
- package/dist/core/thought.d.ts.map +1 -0
- package/dist/core/thought.js +1 -0
- package/dist/di/Container.d.ts +226 -0
- package/dist/di/Container.d.ts.map +1 -0
- package/dist/di/Container.js +96 -0
- package/dist/di/Container.js.map +1 -0
- package/dist/di/ServiceRegistry.d.ts +32 -0
- package/dist/di/ServiceRegistry.d.ts.map +1 -0
- package/dist/di/ServiceRegistry.js +1 -0
- package/dist/errors.d.ts +482 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +108 -0
- package/dist/errors.js.map +1 -0
- package/dist/health/HealthChecker.d.ts +73 -0
- package/dist/health/HealthChecker.d.ts.map +1 -0
- package/dist/health/HealthChecker.js +69 -0
- package/dist/health/HealthChecker.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib.d.ts +205 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +219 -0
- package/dist/lib.js.map +1 -0
- package/dist/logger/NullLogger.d.ts +154 -0
- package/dist/logger/NullLogger.d.ts.map +1 -0
- package/dist/logger/NullLogger.js +24 -0
- package/dist/logger/NullLogger.js.map +1 -0
- package/dist/logger/StructuredLogger.d.ts +327 -0
- package/dist/logger/StructuredLogger.d.ts.map +1 -0
- package/dist/logger/StructuredLogger.js +72 -0
- package/dist/logger/StructuredLogger.js.map +1 -0
- package/dist/metrics/__tests__/metrics.test.d.ts +2 -0
- package/dist/metrics/__tests__/metrics.test.d.ts.map +1 -0
- package/dist/metrics/metrics.impl.d.ts +252 -0
- package/dist/metrics/metrics.impl.d.ts.map +1 -0
- package/dist/metrics/metrics.impl.js +197 -0
- package/dist/metrics/metrics.impl.js.map +1 -0
- package/dist/persistence/FilePersistence.d.ts +66 -0
- package/dist/persistence/FilePersistence.d.ts.map +1 -0
- package/dist/persistence/FilePersistence.js +132 -0
- package/dist/persistence/FilePersistence.js.map +1 -0
- package/dist/persistence/MemoryPersistence.d.ts +68 -0
- package/dist/persistence/MemoryPersistence.d.ts.map +1 -0
- package/dist/persistence/MemoryPersistence.js +51 -0
- package/dist/persistence/MemoryPersistence.js.map +1 -0
- package/dist/persistence/PersistenceBackend.d.ts +69 -0
- package/dist/persistence/PersistenceBackend.d.ts.map +1 -0
- package/dist/persistence/PersistenceBackend.js +1 -0
- package/dist/persistence/PersistenceFactory.d.ts +21 -0
- package/dist/persistence/PersistenceFactory.d.ts.map +1 -0
- package/dist/persistence/PersistenceFactory.js +25 -0
- package/dist/persistence/PersistenceFactory.js.map +1 -0
- package/dist/persistence/SqlitePersistence.d.ts +60 -0
- package/dist/persistence/SqlitePersistence.d.ts.map +1 -0
- package/dist/persistence/SqlitePersistence.js +136 -0
- package/dist/persistence/SqlitePersistence.js.map +1 -0
- package/dist/pool/ConnectionPool.d.ts +215 -0
- package/dist/pool/ConnectionPool.d.ts.map +1 -0
- package/dist/pool/ConnectionPool.js +187 -0
- package/dist/pool/ConnectionPool.js.map +1 -0
- package/dist/registry/BaseRegistry.d.ts +203 -0
- package/dist/registry/BaseRegistry.d.ts.map +1 -0
- package/dist/registry/BaseRegistry.js +165 -0
- package/dist/registry/BaseRegistry.js.map +1 -0
- package/dist/registry/SkillRegistry.d.ts +69 -0
- package/dist/registry/SkillRegistry.d.ts.map +1 -0
- package/dist/registry/SkillRegistry.js +88 -0
- package/dist/registry/SkillRegistry.js.map +1 -0
- package/dist/registry/ToolRegistry.d.ts +69 -0
- package/dist/registry/ToolRegistry.d.ts.map +1 -0
- package/dist/registry/ToolRegistry.js +93 -0
- package/dist/registry/ToolRegistry.js.map +1 -0
- package/dist/sanitize.d.ts +63 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +14 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/schema.d.ts +531 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +204 -0
- package/dist/schema.js.map +1 -0
- package/dist/telemetry/Telemetry.d.ts +36 -0
- package/dist/telemetry/Telemetry.d.ts.map +1 -0
- package/dist/telemetry/Telemetry.js +68 -0
- package/dist/telemetry/Telemetry.js.map +1 -0
- package/dist/telemetry/__tests__/Telemetry.test.d.ts +2 -0
- package/dist/telemetry/__tests__/Telemetry.test.d.ts.map +1 -0
- package/dist/transport/BaseTransport.d.ts +184 -0
- package/dist/transport/BaseTransport.d.ts.map +1 -0
- package/dist/transport/BaseTransport.js +200 -0
- package/dist/transport/BaseTransport.js.map +1 -0
- package/dist/transport/HttpHelpers.d.ts +60 -0
- package/dist/transport/HttpHelpers.d.ts.map +1 -0
- package/dist/transport/HttpHelpers.js +50 -0
- package/dist/transport/HttpHelpers.js.map +1 -0
- package/dist/transport/HttpTransport.d.ts +134 -0
- package/dist/transport/HttpTransport.d.ts.map +1 -0
- package/dist/transport/HttpTransport.js +175 -0
- package/dist/transport/HttpTransport.js.map +1 -0
- package/dist/transport/SseTransport.d.ts +133 -0
- package/dist/transport/SseTransport.d.ts.map +1 -0
- package/dist/transport/SseTransport.js +318 -0
- package/dist/transport/SseTransport.js.map +1 -0
- package/dist/transport/StreamableHttpTransport.d.ts +224 -0
- package/dist/transport/StreamableHttpTransport.d.ts.map +1 -0
- package/dist/transport/StreamableHttpTransport.js +407 -0
- package/dist/transport/StreamableHttpTransport.js.map +1 -0
- package/dist/types/disposable.d.ts +22 -0
- package/dist/types/disposable.d.ts.map +1 -0
- package/dist/types/disposable.js +1 -0
- package/dist/types/server-config.d.ts +32 -0
- package/dist/types/server-config.d.ts.map +1 -0
- package/dist/types/server-config.js +1 -0
- package/dist/types/skill.d.ts +69 -0
- package/dist/types/skill.d.ts.map +1 -0
- package/dist/types/skill.js +1 -0
- package/dist/types/tool.d.ts +68 -0
- package/dist/types/tool.d.ts.map +1 -0
- package/dist/types/tool.js +1 -0
- package/dist/watchers/SkillWatcher.d.ts +132 -0
- package/dist/watchers/SkillWatcher.d.ts.map +1 -0
- package/dist/watchers/SkillWatcher.js +73 -0
- package/dist/watchers/SkillWatcher.js.map +1 -0
- package/dist/watchers/ToolWatcher.d.ts +109 -0
- package/dist/watchers/ToolWatcher.d.ts.map +1 -0
- package/dist/watchers/ToolWatcher.js +71 -0
- package/dist/watchers/ToolWatcher.js.map +1 -0
- 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
|