@iworkforces/tracelattice 2.0.6 → 2.0.8
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 +28 -1
- package/dist/cli.js +296 -196
- package/dist/contracts/summary.d.ts +0 -1
- package/dist/contracts/summary.d.ts.map +1 -1
- package/dist/core/PersistenceRestore.d.ts +1 -1
- package/dist/core/PersistenceRestore.d.ts.map +1 -1
- package/dist/core/PersistenceRestore.js.map +1 -1
- package/dist/core/ThoughtEvaluator.d.ts.map +1 -1
- package/dist/core/ThoughtEvaluator.js.map +1 -1
- package/dist/core/ThoughtFormatter.js.map +1 -1
- package/dist/core/ThoughtProcessor.d.ts.map +1 -1
- package/dist/core/ThoughtProcessor.js.map +1 -1
- package/dist/core/compression/CompressionService.d.ts +6 -4
- package/dist/core/compression/CompressionService.d.ts.map +1 -1
- package/dist/core/compression/CompressionService.js +19 -10
- package/dist/core/compression/CompressionService.js.map +1 -1
- package/dist/core/compression/DehydrationPolicy.d.ts +4 -4
- package/dist/core/compression/DehydrationPolicy.d.ts.map +1 -1
- package/dist/core/compression/DehydrationPolicy.js +3 -6
- package/dist/core/compression/DehydrationPolicy.js.map +1 -1
- package/dist/core/compression/InMemorySummaryStore.d.ts +2 -1
- package/dist/core/compression/InMemorySummaryStore.d.ts.map +1 -1
- package/dist/core/compression/InMemorySummaryStore.js.map +1 -1
- package/dist/core/evaluator/Calibrator.d.ts +1 -1
- package/dist/core/evaluator/Calibrator.d.ts.map +1 -1
- package/dist/core/evaluator/Calibrator.js.map +1 -1
- package/dist/core/evaluator/SignalComputer.d.ts +2 -1
- package/dist/core/evaluator/SignalComputer.d.ts.map +1 -1
- package/dist/core/evaluator/SignalComputer.js.map +1 -1
- package/dist/core/evaluator/internals.d.ts +1 -1
- package/dist/core/evaluator/internals.d.ts.map +1 -1
- package/dist/core/evaluator/internals.js.map +1 -1
- package/dist/core/graph/GraphView.d.ts +13 -1
- package/dist/core/graph/GraphView.d.ts.map +1 -1
- package/dist/core/graph/GraphView.js +34 -0
- package/dist/core/graph/GraphView.js.map +1 -1
- package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.d.ts +6 -6
- package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.d.ts.map +1 -1
- package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.js +16 -2
- package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.js.map +1 -1
- package/dist/core/reasoning/strategies/totScoring.d.ts.map +1 -1
- package/dist/core/reasoning/strategies/totScoring.js.map +1 -1
- package/dist/core/reasoning.d.ts +1 -3
- package/dist/core/reasoning.d.ts.map +1 -1
- package/dist/core/thought.d.ts +1 -1
- package/dist/core/thought.d.ts.map +1 -1
- package/dist/lib.d.ts +20 -6
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +52 -3
- package/dist/lib.js.map +1 -1
- package/dist/transport/HttpHelpers.d.ts +4 -1
- package/dist/transport/HttpHelpers.d.ts.map +1 -1
- package/dist/transport/HttpHelpers.js +61 -11
- package/dist/transport/HttpHelpers.js.map +1 -1
- package/dist/transport/HttpRequestLifecycle.d.ts +15 -1
- package/dist/transport/HttpRequestLifecycle.d.ts.map +1 -1
- package/dist/transport/HttpRequestLifecycle.js +66 -4
- package/dist/transport/HttpRequestLifecycle.js.map +1 -1
- package/dist/transport/HttpTransport.d.ts +2 -0
- package/dist/transport/HttpTransport.d.ts.map +1 -1
- package/dist/transport/HttpTransport.js +75 -38
- package/dist/transport/HttpTransport.js.map +1 -1
- package/dist/transport/StreamableHttpTransport.d.ts +17 -29
- package/dist/transport/StreamableHttpTransport.d.ts.map +1 -1
- package/dist/transport/StreamableHttpTransport.js +153 -124
- package/dist/transport/StreamableHttpTransport.js.map +1 -1
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as __rspack_external_valibot from "valibot";
|
|
3
3
|
import * as __rspack_external_node_crypto_2e7c4b46 from "node:crypto";
|
|
4
4
|
import * as __rspack_external_node_http_e245b239 from "node:http";
|
|
5
|
+
import * as __rspack_external_node_string_decoder_69b9bd04 from "node:string_decoder";
|
|
5
6
|
import * as __rspack_external_node_async_hooks_4d8b1b4a from "node:async_hooks";
|
|
6
7
|
import * as __rspack_external__tmcp_adapter_valibot_0faddfa6 from "@tmcp/adapter-valibot";
|
|
7
8
|
import * as __rspack_external__tmcp_transport_stdio_98e5584f from "@tmcp/transport-stdio";
|
|
@@ -1326,7 +1327,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
1326
1327
|
|
|
1327
1328
|
|
|
1328
1329
|
},
|
|
1329
|
-
"
|
|
1330
|
+
"q"(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
1330
1331
|
|
|
1331
1332
|
// EXPORTS
|
|
1332
1333
|
__webpack_require__.d(__webpack_exports__, {
|
|
@@ -1779,6 +1780,8 @@ class BaseTransport {
|
|
|
1779
1780
|
* instead of casting `'__global__'` ad-hoc.
|
|
1780
1781
|
*/ const GLOBAL_SESSION_ID = asSessionId('__global__');
|
|
1781
1782
|
|
|
1783
|
+
;// CONCATENATED MODULE: external "node:string_decoder"
|
|
1784
|
+
|
|
1782
1785
|
;// CONCATENATED MODULE: ./src/transport/HttpHelpers.ts
|
|
1783
1786
|
/**
|
|
1784
1787
|
* Shared HTTP helper utilities for MCP transport implementations.
|
|
@@ -1788,7 +1791,11 @@ class BaseTransport {
|
|
|
1788
1791
|
* HttpTransport and StreamableHttpTransport.
|
|
1789
1792
|
*
|
|
1790
1793
|
* @module transport/HttpHelpers
|
|
1791
|
-
*/
|
|
1794
|
+
*/
|
|
1795
|
+
class RequestBodyAbortedError extends Error {
|
|
1796
|
+
name = 'RequestBodyAbortedError';
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1792
1799
|
* Send a JSON-RPC 2.0 error response.
|
|
1793
1800
|
*
|
|
1794
1801
|
* Standardizes error response formatting across all transport implementations.
|
|
@@ -1856,23 +1863,118 @@ class BaseTransport {
|
|
|
1856
1863
|
* @param req - The incoming HTTP request
|
|
1857
1864
|
* @param maxBodySize - Maximum allowed body size in bytes (0 = unlimited)
|
|
1858
1865
|
* @returns The body string, or `null` if the body exceeded the size limit
|
|
1859
|
-
*/ async function readRequestBody(req, maxBodySize) {
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1866
|
+
*/ async function readRequestBody(req, maxBodySize, signal) {
|
|
1867
|
+
return new Promise((resolve, reject)=>{
|
|
1868
|
+
let body = '';
|
|
1869
|
+
let bodySize = 0;
|
|
1870
|
+
let settled = false;
|
|
1871
|
+
const decoder = new __rspack_external_node_string_decoder_69b9bd04.StringDecoder('utf8');
|
|
1872
|
+
const cleanup = ()=>{
|
|
1873
|
+
req.off('data', onData);
|
|
1874
|
+
req.off('end', onEnd);
|
|
1875
|
+
req.off('aborted', onAborted);
|
|
1876
|
+
req.off('close', onAborted);
|
|
1877
|
+
req.off('error', onError);
|
|
1878
|
+
signal?.removeEventListener('abort', onAborted);
|
|
1879
|
+
};
|
|
1880
|
+
const onData = (chunk)=>{
|
|
1881
|
+
if (settled) return;
|
|
1882
|
+
const chunkSize = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.byteLength;
|
|
1883
|
+
bodySize += chunkSize;
|
|
1884
|
+
if (maxBodySize > 0 && bodySize > maxBodySize) {
|
|
1885
|
+
settled = true;
|
|
1886
|
+
cleanup();
|
|
1887
|
+
req.resume();
|
|
1888
|
+
resolve(null);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
body += typeof chunk === 'string' ? chunk : decoder.write(chunk);
|
|
1892
|
+
};
|
|
1893
|
+
const onEnd = ()=>{
|
|
1894
|
+
if (settled) return;
|
|
1895
|
+
settled = true;
|
|
1896
|
+
cleanup();
|
|
1897
|
+
resolve(body + decoder.end());
|
|
1898
|
+
};
|
|
1899
|
+
const onAborted = ()=>{
|
|
1900
|
+
if (settled) return;
|
|
1901
|
+
settled = true;
|
|
1902
|
+
cleanup();
|
|
1903
|
+
req.resume();
|
|
1904
|
+
reject(new RequestBodyAbortedError());
|
|
1905
|
+
};
|
|
1906
|
+
const onError = (error)=>{
|
|
1907
|
+
if (settled) return;
|
|
1908
|
+
settled = true;
|
|
1909
|
+
cleanup();
|
|
1910
|
+
reject(error);
|
|
1911
|
+
};
|
|
1912
|
+
req.on('data', onData);
|
|
1913
|
+
req.once('end', onEnd);
|
|
1914
|
+
req.once('aborted', onAborted);
|
|
1915
|
+
req.once('close', onAborted);
|
|
1916
|
+
req.once('error', onError);
|
|
1917
|
+
signal?.addEventListener('abort', onAborted, {
|
|
1918
|
+
once: true
|
|
1919
|
+
});
|
|
1920
|
+
if (signal?.aborted) onAborted();
|
|
1921
|
+
});
|
|
1871
1922
|
}
|
|
1872
1923
|
|
|
1873
1924
|
// EXTERNAL MODULE: ./src/context/RequestContext.ts + 1 modules
|
|
1874
1925
|
var RequestContext = __webpack_require__("i");
|
|
1875
1926
|
;// CONCATENATED MODULE: ./src/transport/HttpRequestLifecycle.ts
|
|
1927
|
+
class TrackedPreDispatchLease {
|
|
1928
|
+
_onRelease;
|
|
1929
|
+
_controller = new AbortController();
|
|
1930
|
+
_settled = Promise.withResolvers();
|
|
1931
|
+
_active = true;
|
|
1932
|
+
_cancellationReason = null;
|
|
1933
|
+
constructor(_onRelease){
|
|
1934
|
+
this._onRelease = _onRelease;
|
|
1935
|
+
}
|
|
1936
|
+
get signal() {
|
|
1937
|
+
return this._controller.signal;
|
|
1938
|
+
}
|
|
1939
|
+
get cancellationReason() {
|
|
1940
|
+
return this._cancellationReason;
|
|
1941
|
+
}
|
|
1942
|
+
get settled() {
|
|
1943
|
+
return this._settled.promise;
|
|
1944
|
+
}
|
|
1945
|
+
cancel(reason) {
|
|
1946
|
+
if (!this._active || this._controller.signal.aborted) return;
|
|
1947
|
+
this._cancellationReason = reason;
|
|
1948
|
+
this._controller.abort();
|
|
1949
|
+
}
|
|
1950
|
+
release() {
|
|
1951
|
+
if (!this._active) return;
|
|
1952
|
+
this._active = false;
|
|
1953
|
+
this._onRelease(this);
|
|
1954
|
+
this._settled.resolve();
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
class PreDispatchTracker {
|
|
1958
|
+
_leases = new Set();
|
|
1959
|
+
_accepting = true;
|
|
1960
|
+
acquire() {
|
|
1961
|
+
if (!this._accepting) return null;
|
|
1962
|
+
const lease = new TrackedPreDispatchLease((released)=>this._leases.delete(released));
|
|
1963
|
+
this._leases.add(lease);
|
|
1964
|
+
return lease;
|
|
1965
|
+
}
|
|
1966
|
+
closeAdmission(reason) {
|
|
1967
|
+
this._accepting = false;
|
|
1968
|
+
for (const lease of this._leases)lease.cancel(reason);
|
|
1969
|
+
}
|
|
1970
|
+
async join() {
|
|
1971
|
+
while(this._leases.size > 0){
|
|
1972
|
+
await Promise.all([
|
|
1973
|
+
...this._leases
|
|
1974
|
+
].map((lease)=>lease.settled));
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1876
1978
|
/** Maximum number of reporting failures retained for diagnostics. */ const LIFECYCLE_REPORTING_FAILURE_CAPACITY = 32;
|
|
1877
1979
|
class LifecycleFailureReporter {
|
|
1878
1980
|
_onFailure;
|
|
@@ -1944,14 +2046,27 @@ class AcceptedWorkTracker {
|
|
|
1944
2046
|
get size() {
|
|
1945
2047
|
return this._work.size;
|
|
1946
2048
|
}
|
|
1947
|
-
|
|
2049
|
+
transfer(lease, start) {
|
|
2050
|
+
lease.signal.throwIfAborted();
|
|
2051
|
+
const reservation = Promise.withResolvers();
|
|
2052
|
+
this._work.add(reservation.promise);
|
|
2053
|
+
lease.release();
|
|
2054
|
+
let work;
|
|
2055
|
+
try {
|
|
2056
|
+
work = start();
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
this._work.delete(reservation.promise);
|
|
2059
|
+
reservation.resolve();
|
|
2060
|
+
throw error;
|
|
2061
|
+
}
|
|
1948
2062
|
const observed = work.catch((error)=>{
|
|
1949
2063
|
this._failureReporter.report(error);
|
|
1950
2064
|
});
|
|
1951
|
-
this._work.add(observed);
|
|
1952
2065
|
void observed.then(()=>{
|
|
1953
|
-
this._work.delete(
|
|
2066
|
+
this._work.delete(reservation.promise);
|
|
2067
|
+
reservation.resolve();
|
|
1954
2068
|
});
|
|
2069
|
+
return work;
|
|
1955
2070
|
}
|
|
1956
2071
|
async join() {
|
|
1957
2072
|
while(this._work.size > 0){
|
|
@@ -1964,12 +2079,8 @@ class AcceptedWorkTracker {
|
|
|
1964
2079
|
/**
|
|
1965
2080
|
* Streamable HTTP Transport implementation (MCP spec recommended transport).
|
|
1966
2081
|
*
|
|
1967
|
-
* This transport implements the MCP Streamable HTTP specification, which replaces
|
|
1968
|
-
* the deprecated SSE transport as the recommended HTTP-based transport since March 2025.
|
|
1969
|
-
*
|
|
1970
2082
|
* Key features:
|
|
1971
2083
|
* - POST /mcp for JSON-RPC requests (main MCP endpoint)
|
|
1972
|
-
* - GET /mcp for optional SSE server-to-client notifications
|
|
1973
2084
|
* - Session management via Mcp-Session-Id header
|
|
1974
2085
|
* - Supports both stateful (session-based) and stateless (per-request) modes
|
|
1975
2086
|
* - Health endpoints (/health, /ready)
|
|
@@ -1993,12 +2104,25 @@ class AcceptedWorkTracker {
|
|
|
1993
2104
|
|
|
1994
2105
|
|
|
1995
2106
|
|
|
2107
|
+
function parseSessionRetentionOptions(options) {
|
|
2108
|
+
const { maxSessions, sessionIdleTimeoutMs, sessionSweepIntervalMs } = options;
|
|
2109
|
+
if (maxSessions === undefined && sessionIdleTimeoutMs === undefined && sessionSweepIntervalMs === undefined) {
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
if (maxSessions === undefined || !Number.isFinite(maxSessions) || !Number.isInteger(maxSessions) || maxSessions <= 0 || sessionIdleTimeoutMs === undefined || !Number.isFinite(sessionIdleTimeoutMs) || !Number.isInteger(sessionIdleTimeoutMs) || sessionIdleTimeoutMs <= 0 || sessionSweepIntervalMs === undefined || !Number.isFinite(sessionSweepIntervalMs) || !Number.isInteger(sessionSweepIntervalMs) || sessionSweepIntervalMs <= 0) {
|
|
2113
|
+
throw new TypeError('maxSessions, sessionIdleTimeoutMs, and sessionSweepIntervalMs must be provided together as positive integers');
|
|
2114
|
+
}
|
|
2115
|
+
return {
|
|
2116
|
+
maxSessions,
|
|
2117
|
+
sessionIdleTimeoutMs,
|
|
2118
|
+
sessionSweepIntervalMs
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
1996
2121
|
/**
|
|
1997
2122
|
* Streamable HTTP Transport for MCP server.
|
|
1998
2123
|
*
|
|
1999
2124
|
* This transport implements the MCP Streamable HTTP specification,
|
|
2000
|
-
* providing JSON-RPC over HTTP with optional session management
|
|
2001
|
-
* and server-to-client SSE notification streams.
|
|
2125
|
+
* providing JSON-RPC over HTTP with optional session management.
|
|
2002
2126
|
*
|
|
2003
2127
|
* @remarks
|
|
2004
2128
|
* **Security Features (inherited from BaseTransport):**
|
|
@@ -2010,13 +2134,11 @@ class AcceptedWorkTracker {
|
|
|
2010
2134
|
*
|
|
2011
2135
|
* **MCP Streamable HTTP Spec Compliance:**
|
|
2012
2136
|
* - POST /mcp — JSON-RPC method calls
|
|
2013
|
-
* - GET /mcp — SSE notification stream (server-to-client)
|
|
2014
2137
|
* - Mcp-Session-Id header for session management
|
|
2015
2138
|
* - Content-Type: application/json for JSON-RPC responses
|
|
2016
|
-
* - Content-Type: text/event-stream for SSE notification streams
|
|
2017
2139
|
*
|
|
2018
2140
|
* **HTTP Status Code Mapping:**
|
|
2019
|
-
* - 200: Success (JSON-RPC response
|
|
2141
|
+
* - 200: Success (JSON-RPC response)
|
|
2020
2142
|
* - 202: Accepted (JSON-RPC notification, no response body)
|
|
2021
2143
|
* - 204: CORS Preflight (empty body)
|
|
2022
2144
|
* - 400: Bad Request (invalid JSON, invalid session ID)
|
|
@@ -2046,8 +2168,12 @@ class AcceptedWorkTracker {
|
|
|
2046
2168
|
_metricsProvider;
|
|
2047
2169
|
_lifecycleFailureReporter;
|
|
2048
2170
|
_acceptedWork;
|
|
2171
|
+
_preDispatch = new PreDispatchTracker();
|
|
2172
|
+
_sessionRetention;
|
|
2173
|
+
_sessionSweepIntervalId = null;
|
|
2049
2174
|
_stopPromise = null;
|
|
2050
2175
|
constructor(options = {}){
|
|
2176
|
+
const sessionRetention = parseSessionRetentionOptions(options);
|
|
2051
2177
|
super(options);
|
|
2052
2178
|
this._path = options.path ?? '/mcp';
|
|
2053
2179
|
this._stateful = options.stateful ?? true;
|
|
@@ -2057,12 +2183,17 @@ class AcceptedWorkTracker {
|
|
|
2057
2183
|
this._requestTimeout = options.requestTimeout ?? 30000;
|
|
2058
2184
|
this._metrics = options.metrics;
|
|
2059
2185
|
this._metricsProvider = options.metricsProvider ?? null;
|
|
2186
|
+
this._sessionRetention = sessionRetention;
|
|
2060
2187
|
this._lifecycleFailureReporter = new LifecycleFailureReporter((error)=>{
|
|
2061
2188
|
this.log('error', 'Streamable HTTP request lifecycle failed', {
|
|
2062
2189
|
error: (0,errors/* .getErrorMessage */.u1)(error)
|
|
2063
2190
|
});
|
|
2064
2191
|
});
|
|
2065
2192
|
this._acceptedWork = new AcceptedWorkTracker(this._lifecycleFailureReporter);
|
|
2193
|
+
if (this._stateful && sessionRetention) {
|
|
2194
|
+
this._sessionSweepIntervalId = setInterval(()=>this._sweepExpiredSessions(), sessionRetention.sessionSweepIntervalMs);
|
|
2195
|
+
this._sessionSweepIntervalId.unref();
|
|
2196
|
+
}
|
|
2066
2197
|
}
|
|
2067
2198
|
_requireMcpServer() {
|
|
2068
2199
|
if (!this._mcpServer) {
|
|
@@ -2120,15 +2251,18 @@ class AcceptedWorkTracker {
|
|
|
2120
2251
|
}
|
|
2121
2252
|
});
|
|
2122
2253
|
}
|
|
2123
|
-
|
|
2124
|
-
* Route and handle incoming HTTP requests.
|
|
2125
|
-
*/ async _handleRequest(req, res) {
|
|
2254
|
+
_recordRequestMetrics(res) {
|
|
2126
2255
|
const startTime = Date.now();
|
|
2127
2256
|
this._metrics?.counter('streamable_http_requests_total', 1, {}, 'Total Streamable HTTP transport requests');
|
|
2128
2257
|
res.once('finish', ()=>{
|
|
2129
2258
|
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
2130
2259
|
this._metrics?.histogram('streamable_http_request_duration_seconds', durationSeconds, {});
|
|
2131
2260
|
});
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Route and handle incoming HTTP requests.
|
|
2264
|
+
*/ async _handleRequest(req, res) {
|
|
2265
|
+
this._recordRequestMetrics(res);
|
|
2132
2266
|
// Host validation
|
|
2133
2267
|
if (!this.validateHostHeader(req)) {
|
|
2134
2268
|
this._sendJsonRpcError(res, 403, -32000, 'Forbidden - invalid host header');
|
|
@@ -2179,16 +2313,20 @@ class AcceptedWorkTracker {
|
|
|
2179
2313
|
// MCP endpoint routing
|
|
2180
2314
|
if (urlPath === this._path) {
|
|
2181
2315
|
if (req.method === 'POST') {
|
|
2182
|
-
this.
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2316
|
+
const lease = this._preDispatch.acquire();
|
|
2317
|
+
if (!lease) {
|
|
2318
|
+
const responseFinalizer = new ResponseFinalizer(res, this._lifecycleFailureReporter);
|
|
2319
|
+
responseFinalizer.finalize((response)=>{
|
|
2320
|
+
this._sendJsonRpcError(response, 503, -32603, 'Server is shutting down');
|
|
2321
|
+
});
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
await this._handleMcpPost(req, res, lease);
|
|
2187
2325
|
return;
|
|
2188
2326
|
}
|
|
2189
2327
|
res.writeHead(405, {
|
|
2190
2328
|
'Content-Type': 'application/json',
|
|
2191
|
-
Allow: '
|
|
2329
|
+
Allow: 'POST'
|
|
2192
2330
|
});
|
|
2193
2331
|
res.end(JSON.stringify({
|
|
2194
2332
|
jsonrpc: '2.0',
|
|
@@ -2222,6 +2360,28 @@ class AcceptedWorkTracker {
|
|
|
2222
2360
|
error
|
|
2223
2361
|
}));
|
|
2224
2362
|
}
|
|
2363
|
+
_parseJsonRpcRequest(body, responseFinalizer) {
|
|
2364
|
+
let rawBody;
|
|
2365
|
+
try {
|
|
2366
|
+
rawBody = JSON.parse(body);
|
|
2367
|
+
} catch {
|
|
2368
|
+
responseFinalizer.finalize((response)=>{
|
|
2369
|
+
this._sendJsonRpcError(response, 200, -32700, 'Parse error');
|
|
2370
|
+
});
|
|
2371
|
+
return null;
|
|
2372
|
+
}
|
|
2373
|
+
const parseResult = (0,external_valibot_.safeParse)(schema/* .JsonRpcRequestSchema */.Uv, rawBody);
|
|
2374
|
+
const rawId = rawBody && typeof rawBody === 'object' && 'id' in rawBody ? rawBody.id ?? null : null;
|
|
2375
|
+
if (!parseResult.success) {
|
|
2376
|
+
responseFinalizer.finalize((response)=>{
|
|
2377
|
+
this._sendJsonRpcError(response, 200, -32600, 'Invalid Request', rawId, {
|
|
2378
|
+
data: parseResult.issues
|
|
2379
|
+
});
|
|
2380
|
+
});
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
return parseResult.output;
|
|
2384
|
+
}
|
|
2225
2385
|
/**
|
|
2226
2386
|
* Handle POST /mcp — JSON-RPC method calls.
|
|
2227
2387
|
*
|
|
@@ -2230,77 +2390,96 @@ class AcceptedWorkTracker {
|
|
|
2230
2390
|
* - Returns Mcp-Session-Id header for new sessions (stateful mode)
|
|
2231
2391
|
* - Validates Mcp-Session-Id for existing sessions (stateful mode)
|
|
2232
2392
|
* - Content-Type: application/json for responses
|
|
2233
|
-
*/ async _handleMcpPost(req, res) {
|
|
2393
|
+
*/ async _handleMcpPost(req, res, lease) {
|
|
2234
2394
|
this._requestCount++;
|
|
2235
2395
|
this._activeRequests++;
|
|
2236
2396
|
const responseFinalizer = new ResponseFinalizer(res, this._lifecycleFailureReporter);
|
|
2397
|
+
let acceptedSession = null;
|
|
2398
|
+
const onAborted = ()=>lease.cancel('peer');
|
|
2399
|
+
req.once('aborted', onAborted);
|
|
2237
2400
|
const timeout = setTimeout(()=>{
|
|
2238
2401
|
responseFinalizer.finalize((response)=>{
|
|
2239
2402
|
this._sendJsonRpcError(response, 500, -32603, 'Request timeout');
|
|
2240
2403
|
});
|
|
2404
|
+
lease.cancel('timeout');
|
|
2241
2405
|
}, this._requestTimeout);
|
|
2242
2406
|
try {
|
|
2243
2407
|
// Read request body with size limit
|
|
2244
|
-
const body = await readRequestBody(req, this._bodySizeLimitEnabled ? this._maxBodySize : 0);
|
|
2408
|
+
const body = await readRequestBody(req, this._bodySizeLimitEnabled ? this._maxBodySize : 0, lease.signal);
|
|
2245
2409
|
if (body === null) {
|
|
2246
2410
|
responseFinalizer.finalize((response)=>{
|
|
2247
2411
|
this._sendJsonRpcError(response, 413, -32000, 'Request body too large');
|
|
2248
2412
|
});
|
|
2249
2413
|
return;
|
|
2250
2414
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2415
|
+
const jsonRpcRequest = this._parseJsonRpcRequest(body, responseFinalizer);
|
|
2416
|
+
if (jsonRpcRequest === null) return;
|
|
2417
|
+
const session = this._acceptPostSession(req, responseFinalizer);
|
|
2418
|
+
if (!session) return;
|
|
2419
|
+
acceptedSession = session.state;
|
|
2420
|
+
// Check if MCP server is ready
|
|
2421
|
+
if (!this._mcpServer) {
|
|
2422
|
+
const requestId = 'id' in jsonRpcRequest ? jsonRpcRequest.id ?? null : null;
|
|
2256
2423
|
responseFinalizer.finalize((response)=>{
|
|
2257
|
-
this._sendJsonRpcError(response,
|
|
2424
|
+
this._sendJsonRpcError(response, 503, -32603, 'Server not ready', requestId);
|
|
2258
2425
|
});
|
|
2259
2426
|
return;
|
|
2260
2427
|
}
|
|
2261
|
-
|
|
2262
|
-
const
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
data: parseResult.issues
|
|
2428
|
+
req.off('aborted', onAborted);
|
|
2429
|
+
const acceptedWork = this._acceptedWork.transfer(lease, ()=>this._receiveAndFinalize(jsonRpcRequest, session.id, responseFinalizer).catch((error)=>{
|
|
2430
|
+
responseFinalizer.finalize((response)=>{
|
|
2431
|
+
this._sendJsonRpcError(response, 200, -32603, 'Internal error', null, {
|
|
2432
|
+
data: (0,errors/* .getErrorMessage */.u1)(error)
|
|
2433
|
+
});
|
|
2268
2434
|
});
|
|
2269
|
-
});
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
let sessionId;
|
|
2275
|
-
if (this._stateful) {
|
|
2276
|
-
const sessionResult = this._resolveSession(req);
|
|
2277
|
-
if (typeof sessionResult !== 'string') {
|
|
2435
|
+
}));
|
|
2436
|
+
await acceptedWork;
|
|
2437
|
+
} catch (error) {
|
|
2438
|
+
if (lease.signal.aborted) {
|
|
2439
|
+
if (lease.cancellationReason === 'shutdown') {
|
|
2278
2440
|
responseFinalizer.finalize((response)=>{
|
|
2279
|
-
this._sendJsonRpcError(response,
|
|
2441
|
+
this._sendJsonRpcError(response, 503, -32603, 'Server is shutting down');
|
|
2280
2442
|
});
|
|
2281
|
-
return;
|
|
2282
2443
|
}
|
|
2283
|
-
sessionId = sessionResult;
|
|
2284
|
-
}
|
|
2285
|
-
// Check if MCP server is ready
|
|
2286
|
-
if (!this._mcpServer) {
|
|
2287
|
-
responseFinalizer.finalize((response)=>{
|
|
2288
|
-
this._sendJsonRpcError(response, 503, -32603, 'Server not ready', jsonRpcRequest?.id ?? null);
|
|
2289
|
-
});
|
|
2290
2444
|
return;
|
|
2291
2445
|
}
|
|
2292
|
-
await this._receiveAndFinalize(jsonRpcRequest, sessionId, responseFinalizer);
|
|
2293
|
-
} catch (error) {
|
|
2294
2446
|
responseFinalizer.finalize((response)=>{
|
|
2295
2447
|
this._sendJsonRpcError(response, 200, -32603, 'Internal error', null, {
|
|
2296
2448
|
data: (0,errors/* .getErrorMessage */.u1)(error)
|
|
2297
2449
|
});
|
|
2298
2450
|
});
|
|
2299
2451
|
} finally{
|
|
2452
|
+
req.off('aborted', onAborted);
|
|
2453
|
+
lease.release();
|
|
2454
|
+
if (acceptedSession) acceptedSession.acceptedPostCount--;
|
|
2300
2455
|
clearTimeout(timeout);
|
|
2301
2456
|
this._activeRequests--;
|
|
2302
2457
|
}
|
|
2303
2458
|
}
|
|
2459
|
+
_acceptPostSession(req, responseFinalizer) {
|
|
2460
|
+
if (!this._stateful) return {
|
|
2461
|
+
id: undefined,
|
|
2462
|
+
state: null
|
|
2463
|
+
};
|
|
2464
|
+
if (this.isShuttingDown) {
|
|
2465
|
+
responseFinalizer.finalize((response)=>{
|
|
2466
|
+
this._sendJsonRpcError(response, 503, -32603, 'Server is shutting down');
|
|
2467
|
+
});
|
|
2468
|
+
return null;
|
|
2469
|
+
}
|
|
2470
|
+
const session = this._resolveSession(req);
|
|
2471
|
+
if (!('state' in session)) {
|
|
2472
|
+
responseFinalizer.finalize((response)=>{
|
|
2473
|
+
this._sendJsonRpcError(response, session.statusCode, session.code, session.message);
|
|
2474
|
+
});
|
|
2475
|
+
return null;
|
|
2476
|
+
}
|
|
2477
|
+
session.state.acceptedPostCount++;
|
|
2478
|
+
return {
|
|
2479
|
+
id: session.id,
|
|
2480
|
+
state: session.state
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2304
2483
|
async _receiveAndFinalize(jsonRpcRequest, sessionId, responseFinalizer) {
|
|
2305
2484
|
const owner = this._stateful ? sessionId ?? (0,__rspack_external_node_crypto_2e7c4b46.randomUUID)() : (0,__rspack_external_node_crypto_2e7c4b46.randomUUID)();
|
|
2306
2485
|
const response = await (0,RequestContext/* .runWithContext */.Vo)({
|
|
@@ -2324,82 +2503,6 @@ class AcceptedWorkTracker {
|
|
|
2324
2503
|
httpResponse.end();
|
|
2325
2504
|
});
|
|
2326
2505
|
}
|
|
2327
|
-
/**
|
|
2328
|
-
* Handle GET /mcp — Optional SSE notification stream.
|
|
2329
|
-
*
|
|
2330
|
-
* Per the MCP Streamable HTTP spec, clients may open a GET request
|
|
2331
|
-
* to receive server-initiated notifications as SSE events.
|
|
2332
|
-
* Requires a valid Mcp-Session-Id in stateful mode.
|
|
2333
|
-
*/ _handleMcpGet(req, res) {
|
|
2334
|
-
if (!this._stateful) {
|
|
2335
|
-
// SSE notification streams require stateful mode
|
|
2336
|
-
res.writeHead(405, {
|
|
2337
|
-
'Content-Type': 'application/json',
|
|
2338
|
-
Allow: 'POST'
|
|
2339
|
-
});
|
|
2340
|
-
res.end(JSON.stringify({
|
|
2341
|
-
jsonrpc: '2.0',
|
|
2342
|
-
id: null,
|
|
2343
|
-
error: {
|
|
2344
|
-
code: -32601,
|
|
2345
|
-
message: 'GET not supported in stateless mode'
|
|
2346
|
-
}
|
|
2347
|
-
}));
|
|
2348
|
-
return;
|
|
2349
|
-
}
|
|
2350
|
-
// Require Mcp-Session-Id for GET requests
|
|
2351
|
-
const sessionId = this._getSessionIdFromHeader(req);
|
|
2352
|
-
if (!sessionId) {
|
|
2353
|
-
res.writeHead(400, {
|
|
2354
|
-
'Content-Type': 'application/json'
|
|
2355
|
-
});
|
|
2356
|
-
res.end(JSON.stringify({
|
|
2357
|
-
jsonrpc: '2.0',
|
|
2358
|
-
id: null,
|
|
2359
|
-
error: {
|
|
2360
|
-
code: -32600,
|
|
2361
|
-
message: 'Missing Mcp-Session-Id header'
|
|
2362
|
-
}
|
|
2363
|
-
}));
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
const session = this._sessions.get(asSessionId(sessionId));
|
|
2367
|
-
if (!session) {
|
|
2368
|
-
res.writeHead(404, {
|
|
2369
|
-
'Content-Type': 'application/json'
|
|
2370
|
-
});
|
|
2371
|
-
res.end(JSON.stringify({
|
|
2372
|
-
jsonrpc: '2.0',
|
|
2373
|
-
id: null,
|
|
2374
|
-
error: {
|
|
2375
|
-
code: -32001,
|
|
2376
|
-
message: 'Session not found'
|
|
2377
|
-
}
|
|
2378
|
-
}));
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2381
|
-
// Set SSE headers
|
|
2382
|
-
res.writeHead(200, {
|
|
2383
|
-
'Content-Type': 'text/event-stream',
|
|
2384
|
-
'Cache-Control': 'no-cache',
|
|
2385
|
-
Connection: 'keep-alive',
|
|
2386
|
-
'Mcp-Session-Id': sessionId
|
|
2387
|
-
});
|
|
2388
|
-
// Send initial connected event
|
|
2389
|
-
this._sendSseEvent(res, 'connected', {
|
|
2390
|
-
sessionId,
|
|
2391
|
-
timestamp: Date.now()
|
|
2392
|
-
});
|
|
2393
|
-
// Track this notification stream
|
|
2394
|
-
session.notificationStreams.add(res);
|
|
2395
|
-
session.lastActivityAt = Date.now();
|
|
2396
|
-
this._updateSessionMetrics();
|
|
2397
|
-
// Handle client disconnect
|
|
2398
|
-
req.on('close', ()=>{
|
|
2399
|
-
session.notificationStreams.delete(res);
|
|
2400
|
-
this._updateSessionMetrics();
|
|
2401
|
-
});
|
|
2402
|
-
}
|
|
2403
2506
|
/**
|
|
2404
2507
|
* Resolve or create a session for a stateful request.
|
|
2405
2508
|
*
|
|
@@ -2419,7 +2522,10 @@ class AcceptedWorkTracker {
|
|
|
2419
2522
|
const session = this._sessions.get(asSessionId(headerSessionId));
|
|
2420
2523
|
if (session) {
|
|
2421
2524
|
session.lastActivityAt = Date.now();
|
|
2422
|
-
return
|
|
2525
|
+
return {
|
|
2526
|
+
id: session.id,
|
|
2527
|
+
state: session
|
|
2528
|
+
};
|
|
2423
2529
|
}
|
|
2424
2530
|
// Unknown session ID — per spec, return 404
|
|
2425
2531
|
return {
|
|
@@ -2429,17 +2535,48 @@ class AcceptedWorkTracker {
|
|
|
2429
2535
|
};
|
|
2430
2536
|
}
|
|
2431
2537
|
// No session header — create new session
|
|
2432
|
-
|
|
2538
|
+
if (this._sessionRetention && this._sessions.size >= this._sessionRetention.maxSessions) {
|
|
2539
|
+
return {
|
|
2540
|
+
statusCode: 503,
|
|
2541
|
+
code: -32000,
|
|
2542
|
+
message: 'Session capacity reached'
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
const newSessionId = asSessionId(this._sessionIdGenerator());
|
|
2546
|
+
const now = Date.now();
|
|
2433
2547
|
const sessionState = {
|
|
2434
2548
|
id: newSessionId,
|
|
2435
|
-
createdAt:
|
|
2436
|
-
lastActivityAt:
|
|
2437
|
-
|
|
2549
|
+
createdAt: now,
|
|
2550
|
+
lastActivityAt: now,
|
|
2551
|
+
acceptedPostCount: 0
|
|
2438
2552
|
};
|
|
2439
|
-
this._sessions.set(
|
|
2553
|
+
this._sessions.set(newSessionId, sessionState);
|
|
2440
2554
|
this.log('info', `New session created: ${newSessionId}`);
|
|
2441
2555
|
this._updateSessionMetrics();
|
|
2442
|
-
return
|
|
2556
|
+
return {
|
|
2557
|
+
id: newSessionId,
|
|
2558
|
+
state: sessionState
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
_sweepExpiredSessions() {
|
|
2562
|
+
if (!this._sessionRetention) return;
|
|
2563
|
+
const now = Date.now();
|
|
2564
|
+
for (const [sessionId, session] of this._sessions){
|
|
2565
|
+
if (session.acceptedPostCount === 0 && now - session.lastActivityAt >= this._sessionRetention.sessionIdleTimeoutMs) {
|
|
2566
|
+
this._disposeSession(sessionId);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
_disposeSession(sessionId) {
|
|
2571
|
+
if (!this._sessions.delete(sessionId)) return;
|
|
2572
|
+
this._updateSessionMetrics();
|
|
2573
|
+
}
|
|
2574
|
+
_stopSessionSweep() {
|
|
2575
|
+
const interval = this._sessionSweepIntervalId;
|
|
2576
|
+
if (!interval) return;
|
|
2577
|
+
interval.unref();
|
|
2578
|
+
clearInterval(interval);
|
|
2579
|
+
this._sessionSweepIntervalId = null;
|
|
2443
2580
|
}
|
|
2444
2581
|
/**
|
|
2445
2582
|
* Extract Mcp-Session-Id from request headers.
|
|
@@ -2450,31 +2587,6 @@ class AcceptedWorkTracker {
|
|
|
2450
2587
|
}
|
|
2451
2588
|
return undefined;
|
|
2452
2589
|
}
|
|
2453
|
-
/**
|
|
2454
|
-
* Send an SSE event to a specific client response stream.
|
|
2455
|
-
*/ _sendSseEvent(res, event, data) {
|
|
2456
|
-
try {
|
|
2457
|
-
res.write(`event: ${event}\n`);
|
|
2458
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
2459
|
-
} catch {
|
|
2460
|
-
// Client disconnected — ignore
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
/**
|
|
2464
|
-
* Broadcast a notification to all SSE streams in a given session.
|
|
2465
|
-
*
|
|
2466
|
-
* @param sessionId - Target session ID
|
|
2467
|
-
* @param event - SSE event name
|
|
2468
|
-
* @param data - Event payload
|
|
2469
|
-
*/ broadcastToSession(sessionId, event, data) {
|
|
2470
|
-
const session = this._sessions.get(asSessionId(sessionId));
|
|
2471
|
-
if (!session) {
|
|
2472
|
-
return;
|
|
2473
|
-
}
|
|
2474
|
-
for (const stream of session.notificationStreams){
|
|
2475
|
-
this._sendSseEvent(stream, event, data);
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
2590
|
/**
|
|
2479
2591
|
* Handle GET /metrics endpoint.
|
|
2480
2592
|
*/ _handleMetrics(res) {
|
|
@@ -2533,11 +2645,6 @@ class AcceptedWorkTracker {
|
|
|
2533
2645
|
* Update session-related metrics.
|
|
2534
2646
|
*/ _updateSessionMetrics() {
|
|
2535
2647
|
this._metrics?.gauge('streamable_http_active_sessions', this._sessions.size, {}, 'Active Streamable HTTP sessions');
|
|
2536
|
-
let totalStreams = 0;
|
|
2537
|
-
for (const session of this._sessions.values()){
|
|
2538
|
-
totalStreams += session.notificationStreams.size;
|
|
2539
|
-
}
|
|
2540
|
-
this._metrics?.gauge('streamable_http_notification_streams', totalStreams, {}, 'Active SSE notification streams');
|
|
2541
2648
|
}
|
|
2542
2649
|
/**
|
|
2543
2650
|
* Stop transport server with graceful shutdown.
|
|
@@ -2546,20 +2653,13 @@ class AcceptedWorkTracker {
|
|
|
2546
2653
|
*/ stop(timeout) {
|
|
2547
2654
|
if (this._stopPromise) return this._stopPromise;
|
|
2548
2655
|
this._isShuttingDown = true;
|
|
2656
|
+
this._preDispatch.closeAdmission('shutdown');
|
|
2549
2657
|
this._stopRateLimitCleanup();
|
|
2658
|
+
this._stopSessionSweep();
|
|
2550
2659
|
const shutdownTimeout = timeout ?? 30000;
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
for (const stream of session.notificationStreams){
|
|
2554
|
-
try {
|
|
2555
|
-
stream.end();
|
|
2556
|
-
} catch {
|
|
2557
|
-
// Ignore errors
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
session.notificationStreams.clear();
|
|
2660
|
+
for (const sessionId of this._sessions.keys()){
|
|
2661
|
+
this._disposeSession(sessionId);
|
|
2561
2662
|
}
|
|
2562
|
-
this._sessions.clear();
|
|
2563
2663
|
const server = this._server;
|
|
2564
2664
|
const serverClosed = new Promise((resolve, reject)=>{
|
|
2565
2665
|
if (!server?.listening) {
|
|
@@ -2589,7 +2689,7 @@ class AcceptedWorkTracker {
|
|
|
2589
2689
|
});
|
|
2590
2690
|
const stopPromise = Promise.all([
|
|
2591
2691
|
serverClosed,
|
|
2592
|
-
this._acceptedWork.join()
|
|
2692
|
+
this._preDispatch.join().then(()=>this._acceptedWork.join())
|
|
2593
2693
|
]).then(()=>{
|
|
2594
2694
|
this.log('info', 'Streamable HTTP transport stopped');
|
|
2595
2695
|
});
|
|
@@ -3103,7 +3203,7 @@ async function main() {
|
|
|
3103
3203
|
/**
|
|
3104
3204
|
* Start Streamable HTTP transport (MCP spec recommended)
|
|
3105
3205
|
*/ async function startStreamableHttpTransport(server, thinkingServer, lifecycle) {
|
|
3106
|
-
const { StreamableHttpTransport } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, "
|
|
3206
|
+
const { StreamableHttpTransport } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, "q"));
|
|
3107
3207
|
const port = parseInt(process.env.STREAMABLE_HTTP_PORT || '9007', 10);
|
|
3108
3208
|
const host = process.env.STREAMABLE_HTTP_HOST || 'localhost';
|
|
3109
3209
|
const transportMetrics = thinkingServer.getContainer().resolve('Metrics');
|