@iworkforces/tracelattice 2.0.3 → 2.0.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/README.md +6 -12
- package/dist/cli.js +221 -2213
- package/dist/cli.js.map +1 -1
- package/dist/contracts/transport.d.ts +2 -2
- package/dist/contracts/transport.d.ts.map +1 -1
- package/dist/core/ThoughtProcessor.d.ts +1 -1
- package/dist/core/ThoughtProcessor.d.ts.map +1 -1
- package/dist/core/ThoughtProcessor.js +3 -6
- package/dist/core/ThoughtProcessor.js.map +1 -1
- package/dist/lib.d.ts +0 -10
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/persistence/FileSnapshotV2.d.ts +2 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +4 -2
- package/dist/schema.js.map +1 -1
- package/dist/transport/HttpHelpers.d.ts +1 -1
- package/dist/transport/HttpHelpers.js.map +1 -1
- package/package.json +1 -1
- package/dist/transport/SseTransport.d.ts +0 -149
- package/dist/transport/SseTransport.d.ts.map +0 -1
- package/dist/transport/SseTransport.js +0 -470
- package/dist/transport/SseTransport.js.map +0 -1
|
@@ -1,149 +0,0 @@
|
|
|
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/interfaces.js';
|
|
21
|
-
import type { PersistenceConfig } from '../contracts/PersistenceBackend.js';
|
|
22
|
-
import type { IConnectionPool } from '../pool/IConnectionPool.js';
|
|
23
|
-
import { BaseTransport, type TransportOptions } from './BaseTransport.js';
|
|
24
|
-
import type { ITransport, TransportKind } from '../contracts/transport.js';
|
|
25
|
-
/**
|
|
26
|
-
* SSE-specific transport options extending base TransportOptions.
|
|
27
|
-
*/
|
|
28
|
-
export interface SseTransportOptions extends TransportOptions {
|
|
29
|
-
path?: string;
|
|
30
|
-
metrics?: IMetrics;
|
|
31
|
-
/**
|
|
32
|
-
* Optional connection pool for per-session state isolation.
|
|
33
|
-
* When provided, each SSE client gets an isolated thought history.
|
|
34
|
-
* When omitted, all clients share a single server instance (backward compatible).
|
|
35
|
-
*/
|
|
36
|
-
connectionPool?: IConnectionPool;
|
|
37
|
-
readonly persistence?: PersistenceConfig;
|
|
38
|
-
}
|
|
39
|
-
/** Rejects persistence backends that cannot isolate pooled SSE child servers. */
|
|
40
|
-
export declare function assertPooledSsePersistence(poolingEnabled: boolean, persistence: PersistenceConfig | undefined): void;
|
|
41
|
-
/**
|
|
42
|
-
* SSE Transport for MCP server over HTTP.
|
|
43
|
-
*
|
|
44
|
-
* This transport uses Server-Sent Events (SSE) to communicate with clients,
|
|
45
|
-
* allowing multiple concurrent connections and web-based clients.
|
|
46
|
-
*
|
|
47
|
-
* @remarks
|
|
48
|
-
* **Security Features:**
|
|
49
|
-
* - Session ID validation (alphanumeric, max 64 chars)
|
|
50
|
-
* - Query parameter sanitization (whitelist allowed keys)
|
|
51
|
-
* - Rate limiting per IP (configurable, default 100 req/min)
|
|
52
|
-
* - CORS origin validation
|
|
53
|
-
*
|
|
54
|
-
* **Rate Limiting:**
|
|
55
|
-
* - Tracks requests per IP address within a time window
|
|
56
|
-
* - Returns 429 Too Many Requests when limit exceeded
|
|
57
|
-
* - Can be disabled via `enableRateLimit: false`
|
|
58
|
-
*/
|
|
59
|
-
export declare class SseTransport extends BaseTransport implements ITransport {
|
|
60
|
-
get kind(): TransportKind;
|
|
61
|
-
private _server;
|
|
62
|
-
private _path;
|
|
63
|
-
private _clients;
|
|
64
|
-
private _clientSessionMap;
|
|
65
|
-
private readonly _sessionAttachmentCounts;
|
|
66
|
-
private readonly _releasePromises;
|
|
67
|
-
private readonly _pendingReleases;
|
|
68
|
-
private readonly _releaseFailures;
|
|
69
|
-
private _messageQueue;
|
|
70
|
-
private _metrics?;
|
|
71
|
-
private _connectionPool?;
|
|
72
|
-
private _stopPromise;
|
|
73
|
-
constructor(options?: SseTransportOptions);
|
|
74
|
-
/**
|
|
75
|
-
* Connect MCP server to this transport.
|
|
76
|
-
*
|
|
77
|
-
* @param mcpServer - The MCP server instance
|
|
78
|
-
*/
|
|
79
|
-
connect(mcpServer: McpServer): Promise<void>;
|
|
80
|
-
private _mcpServer;
|
|
81
|
-
/**
|
|
82
|
-
* Handle incoming HTTP requests
|
|
83
|
-
*/
|
|
84
|
-
private _handleRequest;
|
|
85
|
-
private _prepareRequest;
|
|
86
|
-
/**
|
|
87
|
-
* Handle health check (liveness) endpoint
|
|
88
|
-
*/
|
|
89
|
-
private _handleHealthCheck;
|
|
90
|
-
/**
|
|
91
|
-
* Handle readiness check endpoint
|
|
92
|
-
*/
|
|
93
|
-
private _handleReadinessCheck;
|
|
94
|
-
/**
|
|
95
|
-
* Handle new SSE connection
|
|
96
|
-
*/
|
|
97
|
-
private _handleSseConnection;
|
|
98
|
-
/**
|
|
99
|
-
* Handle incoming message from client
|
|
100
|
-
*/
|
|
101
|
-
private _handleMessage;
|
|
102
|
-
private _parseMessage;
|
|
103
|
-
private _writeSessionUnavailable;
|
|
104
|
-
private _releaseClient;
|
|
105
|
-
private _releaseAfterDisconnect;
|
|
106
|
-
/**
|
|
107
|
-
* Send an SSE event to a specific client
|
|
108
|
-
*/
|
|
109
|
-
private _sendSseEvent;
|
|
110
|
-
private _updateActiveConnectionsMetric;
|
|
111
|
-
private _updatePoolMetrics;
|
|
112
|
-
/**
|
|
113
|
-
* Broadcast a message to all connected clients
|
|
114
|
-
*/
|
|
115
|
-
broadcast(event: string, data: unknown): void;
|
|
116
|
-
/**
|
|
117
|
-
* Generate a unique client ID
|
|
118
|
-
*/
|
|
119
|
-
private _generateClientId;
|
|
120
|
-
/**
|
|
121
|
-
* Get number of connected clients
|
|
122
|
-
*/
|
|
123
|
-
get clientCount(): number;
|
|
124
|
-
/**
|
|
125
|
-
* Get the connection pool, if one was configured.
|
|
126
|
-
*/
|
|
127
|
-
get connectionPool(): IConnectionPool | undefined;
|
|
128
|
-
/**
|
|
129
|
-
* Stop the transport server with graceful shutdown.
|
|
130
|
-
*
|
|
131
|
-
* @param timeout - Maximum time to wait for requests to drain (not used for SSE)
|
|
132
|
-
* @returns Promise that resolves when shutdown is complete
|
|
133
|
-
*/
|
|
134
|
-
stop(_timeout?: number): Promise<void>;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Create an SSE transport with given options.
|
|
138
|
-
*
|
|
139
|
-
* @param options - Transport configuration
|
|
140
|
-
* @returns A configured SSE transport
|
|
141
|
-
*
|
|
142
|
-
* @example
|
|
143
|
-
* ```typescript
|
|
144
|
-
* const transport = createSseTransport({ port: 3000 });
|
|
145
|
-
* await transport.connect(mcpServer);
|
|
146
|
-
* ```
|
|
147
|
-
*/
|
|
148
|
-
export declare function createSseTransport(options?: SseTransportOptions): SseTransport;
|
|
149
|
-
//# sourceMappingURL=SseTransport.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SseTransport.d.ts","sourceRoot":"","sources":["../../src/transport/SseTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAIlE,OAAO,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAW3E;;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,eAAe,CAAC;IACjC,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACzC;AAED,iFAAiF;AACjF,wBAAgB,0BAA0B,CACzC,cAAc,EAAE,OAAO,EACvB,WAAW,EAAE,iBAAiB,GAAG,SAAS,GACxC,IAAI,CAQN;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,YAAa,SAAQ,aAAc,YAAW,UAAU;IACpE,IAAI,IAAI,IAAI,aAAa,CAExB;IACD,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,iBAAiB,CAA6C;IACtE,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAgC;IACzE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAgD;IACjF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA4B;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsB;IACvD,OAAO,CAAC,aAAa,CAAqC;IAC1D,OAAO,CAAC,QAAQ,CAAC,CAAW;IAC5B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAA8B;gBAEtC,OAAO,GAAE,mBAAwB;IAW7C;;;;OAIG;IACG,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BlD,OAAO,CAAC,UAAU,CAA0B;IAE5C;;OAEG;YACW,cAAc;IAkC5B,OAAO,CAAC,eAAe;IAuFvB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAc1B;;OAEG;YACW,qBAAqB;IAcnC;;OAEG;YACW,oBAAoB;IAgElC;;OAEG;YACW,cAAc;YA8Ed,aAAa;IAuD3B,OAAO,CAAC,wBAAwB;IAKhC,OAAO,CAAC,cAAc;IAwCtB,OAAO,CAAC,uBAAuB;IAM/B;;OAEG;IACH,OAAO,CAAC,aAAa;IASrB,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,eAAe,GAAG,SAAS,CAEhD;IAED;;;;;OAKG;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAgEtC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,CAElF"}
|
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { URL } from "node:url";
|
|
4
|
-
import { safeParse } from "valibot";
|
|
5
|
-
import { ConfigurationError } from "../errors.js";
|
|
6
|
-
import { SessionNotActiveError, SessionNotFoundError } from "../pool/PoolErrors.js";
|
|
7
|
-
import { JsonRpcRequestSchema } from "../schema.js";
|
|
8
|
-
import { BaseTransport } from "./BaseTransport.js";
|
|
9
|
-
import { asSessionId } from "../contracts/ids.js";
|
|
10
|
-
import { runWithContext } from "../context/RequestContext.js";
|
|
11
|
-
import { assertNever } from "../utils.js";
|
|
12
|
-
function assertPooledSsePersistence(poolingEnabled, persistence) {
|
|
13
|
-
if (poolingEnabled && persistence?.enabled === true && ('file' === persistence.backend || 'sqlite' === persistence.backend)) throw new ConfigurationError(`pooled SSE does not support ${persistence.backend} persistence`);
|
|
14
|
-
}
|
|
15
|
-
class SseTransport extends BaseTransport {
|
|
16
|
-
get kind() {
|
|
17
|
-
return 'sse';
|
|
18
|
-
}
|
|
19
|
-
_server;
|
|
20
|
-
_path;
|
|
21
|
-
_clients = new Set();
|
|
22
|
-
_clientSessionMap = new Map();
|
|
23
|
-
_sessionAttachmentCounts = new Map();
|
|
24
|
-
_releasePromises = new WeakMap();
|
|
25
|
-
_pendingReleases = new Set();
|
|
26
|
-
_releaseFailures = new Set();
|
|
27
|
-
_messageQueue = new Map();
|
|
28
|
-
_metrics;
|
|
29
|
-
_connectionPool;
|
|
30
|
-
_stopPromise = null;
|
|
31
|
-
constructor(options = {}){
|
|
32
|
-
assertPooledSsePersistence(void 0 !== options.connectionPool, options.persistence);
|
|
33
|
-
super(options);
|
|
34
|
-
this._path = options.path ?? '/sse';
|
|
35
|
-
this._metrics = options.metrics;
|
|
36
|
-
this._connectionPool = options.connectionPool;
|
|
37
|
-
this._updateActiveConnectionsMetric();
|
|
38
|
-
this._server = createServer((req, res)=>this._handleRequest(req, res));
|
|
39
|
-
}
|
|
40
|
-
async connect(mcpServer) {
|
|
41
|
-
this._mcpServer = mcpServer;
|
|
42
|
-
return new Promise((resolve, reject)=>{
|
|
43
|
-
const cleanup = ()=>{
|
|
44
|
-
this._server.off('error', onError);
|
|
45
|
-
this._server.off('listening', onListening);
|
|
46
|
-
};
|
|
47
|
-
const onError = (error)=>{
|
|
48
|
-
cleanup();
|
|
49
|
-
reject(error);
|
|
50
|
-
};
|
|
51
|
-
const onListening = ()=>{
|
|
52
|
-
cleanup();
|
|
53
|
-
this.log('info', `SSE transport listening on http://${this._host}:${this._port}`);
|
|
54
|
-
resolve();
|
|
55
|
-
};
|
|
56
|
-
this._server.once('error', onError);
|
|
57
|
-
this._server.once('listening', onListening);
|
|
58
|
-
try {
|
|
59
|
-
this._server.listen(this._port, this._host);
|
|
60
|
-
} catch (error) {
|
|
61
|
-
cleanup();
|
|
62
|
-
reject(error);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
_mcpServer = null;
|
|
67
|
-
async _handleRequest(req, res) {
|
|
68
|
-
const routing = this._prepareRequest(req, res);
|
|
69
|
-
if (!routing) return;
|
|
70
|
-
if (this._enableCors && 'OPTIONS' === req.method) {
|
|
71
|
-
res.writeHead(204);
|
|
72
|
-
res.end();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (routing.url.pathname === this._path && 'GET' === req.method) return void await this._handleSseConnection(req, res, routing.params);
|
|
76
|
-
if (routing.url.pathname === `${this._path}/message` && 'POST' === req.method) return void await this._handleMessage(req, res, routing.params);
|
|
77
|
-
if ('/health' === routing.url.pathname) return void this._handleHealthCheck(res);
|
|
78
|
-
if ('/ready' === routing.url.pathname) return void await this._handleReadinessCheck(res);
|
|
79
|
-
res.writeHead(404, {
|
|
80
|
-
'Content-Type': 'text/plain'
|
|
81
|
-
});
|
|
82
|
-
res.end('Not Found');
|
|
83
|
-
}
|
|
84
|
-
_prepareRequest(req, res) {
|
|
85
|
-
const startTime = Date.now();
|
|
86
|
-
const requestPath = req.url || '/';
|
|
87
|
-
const requestMethod = req.method || 'GET';
|
|
88
|
-
this._metrics?.counter('http_requests_total', 1, {
|
|
89
|
-
transport: 'sse',
|
|
90
|
-
method: requestMethod,
|
|
91
|
-
path: requestPath
|
|
92
|
-
}, 'Total HTTP requests');
|
|
93
|
-
res.once('finish', ()=>{
|
|
94
|
-
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
95
|
-
this._metrics?.histogram('http_request_duration_seconds', durationSeconds, {
|
|
96
|
-
transport: 'sse',
|
|
97
|
-
path: requestPath
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
if (!this.validateHostHeader(req)) {
|
|
101
|
-
this._metrics?.counter('http_request_errors_total', 1, {
|
|
102
|
-
transport: 'sse',
|
|
103
|
-
error_type: 'forbidden'
|
|
104
|
-
}, 'Total HTTP request errors');
|
|
105
|
-
res.writeHead(403, {
|
|
106
|
-
'Content-Type': 'application/json'
|
|
107
|
-
});
|
|
108
|
-
res.end(JSON.stringify({
|
|
109
|
-
error: 'Forbidden - invalid host header'
|
|
110
|
-
}));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
114
|
-
const clientIp = this.getClientIp(req);
|
|
115
|
-
if (this.checkRateLimit(clientIp)) {
|
|
116
|
-
this._metrics?.counter('http_request_errors_total', 1, {
|
|
117
|
-
transport: 'sse',
|
|
118
|
-
error_type: 'rate_limit'
|
|
119
|
-
}, 'Total HTTP request errors');
|
|
120
|
-
res.writeHead(429, {
|
|
121
|
-
'Content-Type': 'application/json',
|
|
122
|
-
'Retry-After': '60'
|
|
123
|
-
});
|
|
124
|
-
res.end(JSON.stringify({
|
|
125
|
-
error: 'Too many requests'
|
|
126
|
-
}));
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (!this.validateCorsOrigin(req)) {
|
|
130
|
-
this._metrics?.counter('http_request_errors_total', 1, {
|
|
131
|
-
transport: 'sse',
|
|
132
|
-
error_type: 'forbidden'
|
|
133
|
-
}, 'Total HTTP request errors');
|
|
134
|
-
res.writeHead(403, {
|
|
135
|
-
'Content-Type': 'application/json'
|
|
136
|
-
});
|
|
137
|
-
res.end(JSON.stringify({
|
|
138
|
-
error: 'Forbidden - invalid origin'
|
|
139
|
-
}));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
this.setCorsHeaders(res);
|
|
143
|
-
const sanitizedParams = this.sanitizeQueryParams(url);
|
|
144
|
-
const requestSessionId = sanitizedParams.session ?? sanitizedParams.sessionId;
|
|
145
|
-
if (void 0 !== requestSessionId && !this.validateSessionId(requestSessionId)) {
|
|
146
|
-
this._metrics?.counter('http_request_errors_total', 1, {
|
|
147
|
-
transport: 'sse',
|
|
148
|
-
error_type: 'validation'
|
|
149
|
-
}, 'Total HTTP request errors');
|
|
150
|
-
res.writeHead(400, {
|
|
151
|
-
'Content-Type': 'application/json'
|
|
152
|
-
});
|
|
153
|
-
res.end(JSON.stringify({
|
|
154
|
-
error: 'Invalid session ID format'
|
|
155
|
-
}));
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
url,
|
|
160
|
-
params: sanitizedParams
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
_handleHealthCheck(res) {
|
|
164
|
-
const healthData = {
|
|
165
|
-
status: 'healthy',
|
|
166
|
-
clients: this._clients.size
|
|
167
|
-
};
|
|
168
|
-
if (this._connectionPool) {
|
|
169
|
-
const poolStats = this._connectionPool.getStats();
|
|
170
|
-
healthData.pool = poolStats;
|
|
171
|
-
}
|
|
172
|
-
if (this._healthChecker) {
|
|
173
|
-
const liveness = this._healthChecker.checkLiveness();
|
|
174
|
-
healthData.liveness = liveness;
|
|
175
|
-
}
|
|
176
|
-
res.writeHead(200, {
|
|
177
|
-
'Content-Type': 'application/json'
|
|
178
|
-
});
|
|
179
|
-
res.end(JSON.stringify(healthData));
|
|
180
|
-
}
|
|
181
|
-
async _handleReadinessCheck(res) {
|
|
182
|
-
if (this._healthChecker) {
|
|
183
|
-
const readiness = await this._healthChecker.checkReadiness();
|
|
184
|
-
const statusCode = 'ok' === readiness.status ? 200 : 503;
|
|
185
|
-
res.writeHead(statusCode, {
|
|
186
|
-
'Content-Type': 'application/json'
|
|
187
|
-
});
|
|
188
|
-
res.end(JSON.stringify(readiness));
|
|
189
|
-
} else {
|
|
190
|
-
res.writeHead(200, {
|
|
191
|
-
'Content-Type': 'application/json'
|
|
192
|
-
});
|
|
193
|
-
res.end(JSON.stringify({
|
|
194
|
-
status: 'ok',
|
|
195
|
-
timestamp: new Date().toISOString(),
|
|
196
|
-
components: {}
|
|
197
|
-
}));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
async _handleSseConnection(req, res, params) {
|
|
201
|
-
res.writeHead(200, {
|
|
202
|
-
'Content-Type': 'text/event-stream',
|
|
203
|
-
'Cache-Control': 'no-cache',
|
|
204
|
-
Connection: 'keep-alive'
|
|
205
|
-
});
|
|
206
|
-
let sessionId;
|
|
207
|
-
if (this._connectionPool) {
|
|
208
|
-
const requestedSession = params.session ?? params.sessionId;
|
|
209
|
-
const requestedSessionId = requestedSession ? asSessionId(requestedSession) : void 0;
|
|
210
|
-
if (requestedSessionId && this._connectionPool.getSessionInfo(requestedSessionId)) sessionId = requestedSessionId;
|
|
211
|
-
else try {
|
|
212
|
-
sessionId = await this._connectionPool.createSession();
|
|
213
|
-
} catch (error) {
|
|
214
|
-
res.write(`event: error\n`);
|
|
215
|
-
res.write(`data: ${JSON.stringify({
|
|
216
|
-
error: error instanceof Error ? error.message : 'Failed to create session'
|
|
217
|
-
})}\n\n`);
|
|
218
|
-
res.end();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
this._clientSessionMap.set(res, sessionId);
|
|
222
|
-
this._sessionAttachmentCounts.set(sessionId, (this._sessionAttachmentCounts.get(sessionId) ?? 0) + 1);
|
|
223
|
-
this._updatePoolMetrics();
|
|
224
|
-
}
|
|
225
|
-
this._clients.add(res);
|
|
226
|
-
this._updateActiveConnectionsMetric();
|
|
227
|
-
const release = ()=>this._releaseAfterDisconnect(res);
|
|
228
|
-
req.once('close', release);
|
|
229
|
-
res.once('close', release);
|
|
230
|
-
res.once('error', release);
|
|
231
|
-
const connectedPayload = {
|
|
232
|
-
timestamp: Date.now()
|
|
233
|
-
};
|
|
234
|
-
if (sessionId) connectedPayload.sessionId = sessionId;
|
|
235
|
-
this._sendSseEvent(res, 'connected', connectedPayload);
|
|
236
|
-
const clientId = this._generateClientId();
|
|
237
|
-
const queued = this._messageQueue.get(clientId);
|
|
238
|
-
if (queued) {
|
|
239
|
-
for (const message of queued)this._sendSseEvent(res, 'message', message);
|
|
240
|
-
this._messageQueue.delete(clientId);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
async _handleMessage(req, res, params) {
|
|
244
|
-
let pooledOwner;
|
|
245
|
-
if (this._connectionPool) {
|
|
246
|
-
const correlation = params.session ?? params.sessionId;
|
|
247
|
-
if (void 0 === correlation) {
|
|
248
|
-
res.writeHead(400, {
|
|
249
|
-
'Content-Type': 'application/json'
|
|
250
|
-
});
|
|
251
|
-
res.end(JSON.stringify({
|
|
252
|
-
error: 'Missing session ID'
|
|
253
|
-
}));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
pooledOwner = asSessionId(correlation);
|
|
257
|
-
}
|
|
258
|
-
const jsonRpcRequest = await this._parseMessage(req, res);
|
|
259
|
-
if (!jsonRpcRequest) return;
|
|
260
|
-
const mcpServer = this._mcpServer;
|
|
261
|
-
if (!mcpServer) {
|
|
262
|
-
this._metrics?.counter('http_request_errors_total', 1, {
|
|
263
|
-
transport: 'sse',
|
|
264
|
-
error_type: 'server_not_ready'
|
|
265
|
-
}, 'Total HTTP request errors');
|
|
266
|
-
res.writeHead(503, {
|
|
267
|
-
'Content-Type': 'application/json'
|
|
268
|
-
});
|
|
269
|
-
res.end(JSON.stringify({
|
|
270
|
-
error: 'Server not ready'
|
|
271
|
-
}));
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const owner = pooledOwner ?? `sse-${randomUUID()}`;
|
|
275
|
-
const receive = ()=>runWithContext({
|
|
276
|
-
requestId: randomUUID(),
|
|
277
|
-
owner
|
|
278
|
-
}, ()=>mcpServer.receive(jsonRpcRequest, {
|
|
279
|
-
sessionInfo: {}
|
|
280
|
-
}));
|
|
281
|
-
try {
|
|
282
|
-
let response;
|
|
283
|
-
if (this._connectionPool && pooledOwner) {
|
|
284
|
-
if ('function' != typeof this._connectionPool.runWithSession) return void this._writeSessionUnavailable(res);
|
|
285
|
-
const result = await this._connectionPool.runWithSession(pooledOwner, receive);
|
|
286
|
-
switch(result.status){
|
|
287
|
-
case 'completed':
|
|
288
|
-
response = result.value;
|
|
289
|
-
break;
|
|
290
|
-
case 'inactive':
|
|
291
|
-
case 'missing':
|
|
292
|
-
this._writeSessionUnavailable(res);
|
|
293
|
-
return;
|
|
294
|
-
default:
|
|
295
|
-
return assertNever(result);
|
|
296
|
-
}
|
|
297
|
-
} else response = await receive();
|
|
298
|
-
res.writeHead(200, {
|
|
299
|
-
'Content-Type': 'application/json'
|
|
300
|
-
});
|
|
301
|
-
res.end(JSON.stringify(response ?? {
|
|
302
|
-
jsonrpc: '2.0',
|
|
303
|
-
id: 'id' in jsonRpcRequest ? jsonRpcRequest.id : null,
|
|
304
|
-
result: null
|
|
305
|
-
}));
|
|
306
|
-
} catch (error) {
|
|
307
|
-
if (error instanceof SessionNotFoundError || error instanceof SessionNotActiveError) return void this._writeSessionUnavailable(res);
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
async _parseMessage(req, res) {
|
|
312
|
-
let body = '';
|
|
313
|
-
for await (const chunk of req)body += chunk.toString();
|
|
314
|
-
let rawBody;
|
|
315
|
-
try {
|
|
316
|
-
rawBody = JSON.parse(body);
|
|
317
|
-
} catch (error) {
|
|
318
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
319
|
-
this._metrics?.counter('http_request_errors_total', 1, {
|
|
320
|
-
transport: 'sse',
|
|
321
|
-
error_type: 'parse_error'
|
|
322
|
-
}, 'Total HTTP request errors');
|
|
323
|
-
res.writeHead(400, {
|
|
324
|
-
'Content-Type': 'application/json'
|
|
325
|
-
});
|
|
326
|
-
res.end(JSON.stringify({
|
|
327
|
-
error: 'Invalid JSON'
|
|
328
|
-
}));
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
const parseResult = safeParse(JsonRpcRequestSchema, rawBody);
|
|
332
|
-
const rawId = rawBody && 'object' == typeof rawBody && 'id' in rawBody ? rawBody.id ?? null : null;
|
|
333
|
-
if (!parseResult.success) {
|
|
334
|
-
this._metrics?.counter('http_request_errors_total', 1, {
|
|
335
|
-
transport: 'sse',
|
|
336
|
-
error_type: 'validation'
|
|
337
|
-
}, 'Total HTTP request errors');
|
|
338
|
-
res.writeHead(200, {
|
|
339
|
-
'Content-Type': 'application/json'
|
|
340
|
-
});
|
|
341
|
-
res.end(JSON.stringify({
|
|
342
|
-
jsonrpc: '2.0',
|
|
343
|
-
id: rawId,
|
|
344
|
-
error: {
|
|
345
|
-
code: -32600,
|
|
346
|
-
message: 'Invalid Request',
|
|
347
|
-
data: parseResult.issues
|
|
348
|
-
}
|
|
349
|
-
}));
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
return parseResult.output;
|
|
353
|
-
}
|
|
354
|
-
_writeSessionUnavailable(res) {
|
|
355
|
-
res.writeHead(404, {
|
|
356
|
-
'Content-Type': 'application/json'
|
|
357
|
-
});
|
|
358
|
-
res.end(JSON.stringify({
|
|
359
|
-
error: 'Session not found'
|
|
360
|
-
}));
|
|
361
|
-
}
|
|
362
|
-
_releaseClient(res) {
|
|
363
|
-
const existing = this._releasePromises.get(res);
|
|
364
|
-
if (existing) return existing;
|
|
365
|
-
this._clients.delete(res);
|
|
366
|
-
const sessionId = this._clientSessionMap.get(res);
|
|
367
|
-
this._clientSessionMap.delete(res);
|
|
368
|
-
this._updateActiveConnectionsMetric();
|
|
369
|
-
let releasePromise = Promise.resolve();
|
|
370
|
-
if (sessionId && this._connectionPool) {
|
|
371
|
-
const attachmentCount = this._sessionAttachmentCounts.get(sessionId);
|
|
372
|
-
if (void 0 !== attachmentCount && attachmentCount > 1) this._sessionAttachmentCounts.set(sessionId, attachmentCount - 1);
|
|
373
|
-
else {
|
|
374
|
-
this._sessionAttachmentCounts.delete(sessionId);
|
|
375
|
-
releasePromise = this._connectionPool.closeSession(sessionId).catch((error)=>{
|
|
376
|
-
if (error instanceof SessionNotFoundError) return;
|
|
377
|
-
throw error;
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
this._updatePoolMetrics();
|
|
381
|
-
}
|
|
382
|
-
this._releasePromises.set(res, releasePromise);
|
|
383
|
-
this._pendingReleases.add(releasePromise);
|
|
384
|
-
releasePromise.then(()=>this._pendingReleases.delete(releasePromise), (error)=>{
|
|
385
|
-
this._pendingReleases.delete(releasePromise);
|
|
386
|
-
this._releaseFailures.add(error);
|
|
387
|
-
});
|
|
388
|
-
return releasePromise;
|
|
389
|
-
}
|
|
390
|
-
_releaseAfterDisconnect(res) {
|
|
391
|
-
this._releaseClient(res).catch((error)=>{
|
|
392
|
-
this.log('error', 'SSE pooled session release failed', {
|
|
393
|
-
error
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
_sendSseEvent(res, event, data) {
|
|
398
|
-
try {
|
|
399
|
-
res.write(`event: ${event}\n`);
|
|
400
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
401
|
-
} catch {
|
|
402
|
-
this._releaseAfterDisconnect(res);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
_updateActiveConnectionsMetric() {
|
|
406
|
-
this._metrics?.gauge('sse_active_connections', this._clients.size, {}, 'Current active SSE connections');
|
|
407
|
-
}
|
|
408
|
-
_updatePoolMetrics() {
|
|
409
|
-
if (!this._connectionPool || !this._metrics) return;
|
|
410
|
-
const stats = this._connectionPool.getStats();
|
|
411
|
-
this._metrics.gauge('sse_pool_active_sessions', stats.activeSessions, {}, 'Active sessions in connection pool');
|
|
412
|
-
this._metrics.gauge('sse_pool_total_sessions', stats.totalSessions, {}, 'Total sessions in connection pool');
|
|
413
|
-
this._metrics.gauge('sse_pool_max_sessions', stats.maxSessions, {}, 'Maximum sessions in connection pool');
|
|
414
|
-
}
|
|
415
|
-
broadcast(event, data) {
|
|
416
|
-
for (const client of this._clients)this._sendSseEvent(client, event, data);
|
|
417
|
-
}
|
|
418
|
-
_generateClientId() {
|
|
419
|
-
return `client_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
420
|
-
}
|
|
421
|
-
get clientCount() {
|
|
422
|
-
return this._clients.size;
|
|
423
|
-
}
|
|
424
|
-
get connectionPool() {
|
|
425
|
-
return this._connectionPool;
|
|
426
|
-
}
|
|
427
|
-
stop(_timeout) {
|
|
428
|
-
if (this._stopPromise) return this._stopPromise;
|
|
429
|
-
const completion = Promise.withResolvers();
|
|
430
|
-
this._stopPromise = completion.promise;
|
|
431
|
-
this._isShuttingDown = true;
|
|
432
|
-
this._stopRateLimitCleanup();
|
|
433
|
-
const operations = new Set();
|
|
434
|
-
for (const client of Array.from(this._clients)){
|
|
435
|
-
operations.add(this._releaseClient(client));
|
|
436
|
-
try {
|
|
437
|
-
client.end();
|
|
438
|
-
} catch (error) {
|
|
439
|
-
operations.add(Promise.reject(error instanceof Error ? error : new AggregateError([
|
|
440
|
-
error
|
|
441
|
-
], 'SSE client close failed')));
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
for (const release of this._pendingReleases)operations.add(release);
|
|
445
|
-
if (this._connectionPool) operations.add(this._connectionPool.terminate());
|
|
446
|
-
operations.add(this._server.listening ? new Promise((resolve, reject)=>{
|
|
447
|
-
this._server.close((error)=>error ? reject(error) : resolve());
|
|
448
|
-
}) : Promise.resolve());
|
|
449
|
-
Promise.allSettled(operations).then((outcomes)=>{
|
|
450
|
-
const failures = new Set(this._releaseFailures);
|
|
451
|
-
for (const outcome of outcomes)if ('rejected' === outcome.status) if (outcome.reason instanceof AggregateError) for (const error of outcome.reason.errors)failures.add(error);
|
|
452
|
-
else failures.add(outcome.reason);
|
|
453
|
-
this._clients.clear();
|
|
454
|
-
this._clientSessionMap.clear();
|
|
455
|
-
this._sessionAttachmentCounts.clear();
|
|
456
|
-
this._updateActiveConnectionsMetric();
|
|
457
|
-
this._updatePoolMetrics();
|
|
458
|
-
if (failures.size > 0) return void completion.reject(new AggregateError(failures, 'SSE transport shutdown failed'));
|
|
459
|
-
this.log('info', 'SSE transport stopped');
|
|
460
|
-
completion.resolve();
|
|
461
|
-
});
|
|
462
|
-
return completion.promise;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
function createSseTransport(options = {}) {
|
|
466
|
-
return new SseTransport(options);
|
|
467
|
-
}
|
|
468
|
-
export { SseTransport, assertPooledSsePersistence, createSseTransport };
|
|
469
|
-
|
|
470
|
-
//# sourceMappingURL=SseTransport.js.map
|