@frontmcp/utils 1.11.2 → 1.12.0-rc.1
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 +4 -2
- package/browser/crypto/encrypted-blob.mjs +1 -1
- package/browser/crypto/hmac-signing.mjs +1 -3
- package/browser/crypto/index.mjs +25 -1
- package/browser/crypto/key-persistence/factory.mjs +2 -2
- package/browser/crypto/key-persistence/key-persistence.mjs +2 -2
- package/browser/crypto/pkce/pkce.mjs +1 -1
- package/browser/crypto/secret-persistence/persistence.mjs +3 -3
- package/browser/crypto/standard-webhooks.mjs +96 -0
- package/browser/fs/index.mjs +3 -0
- package/browser/fs/watch-files.mjs +177 -0
- package/browser/index.mjs +45 -2
- package/browser/net/index.mjs +15 -2
- package/browser/net/ip-address.mjs +30 -0
- package/browser/net/outbound-request.mjs +190 -0
- package/browser/net/outbound-url.mjs +43 -0
- package/browser/serialization/canonical-json.mjs +35 -0
- package/browser/serialization/index.mjs +2 -0
- package/browser/stdio-bridge/claude-code-channels.mjs +73 -0
- package/browser/stdio-bridge/notification-stream.mjs +111 -0
- package/browser/stdio-bridge/sse-frames.mjs +48 -0
- package/browser/stdio-bridge/upstream-client.mjs +89 -37
- package/browser/storage/adapters/filesystem.mjs +3 -3
- package/browser/storage/adapters/indexeddb.mjs +1 -1
- package/browser/storage/adapters/localstorage.mjs +2 -2
- package/browser/storage/encrypted-typed-storage.mjs +1 -1
- package/crypto/index.d.ts +1 -0
- package/crypto/key-persistence/key-persistence.d.ts +1 -1
- package/crypto/standard-webhooks.d.ts +35 -0
- package/env/browser-runtime-context.d.ts +1 -1
- package/env/runtime-context.d.ts +1 -1
- package/esm/index.mjs +1 -1
- package/fs/index.d.ts +2 -0
- package/fs/watch-files.d.ts +22 -0
- package/index.d.ts +6 -6
- package/index.js +1 -1
- package/llm/llm-tool-handlers.d.ts +1 -1
- package/net/index.d.ts +5 -1
- package/net/ip-address.d.ts +2 -0
- package/net/outbound-request.d.ts +46 -0
- package/net/outbound-url.d.ts +18 -0
- package/package.json +3 -3
- package/serialization/canonical-json.d.ts +2 -0
- package/serialization/index.d.ts +1 -0
- package/stdio-bridge/claude-code-channels.d.ts +28 -0
- package/stdio-bridge/notification-stream.d.ts +29 -0
- package/stdio-bridge/sse-frames.d.ts +8 -0
- package/stdio-bridge/upstream-client.d.ts +13 -0
- package/storage/adapters/filesystem.d.ts +1 -1
- package/storage/adapters/indexeddb.d.ts +1 -1
- package/storage/adapters/localstorage.d.ts +1 -1
- package/storage/encrypted-typed-storage.d.ts +2 -2
- package/storage/typed-storage.d.ts +2 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// libs/utils/src/net/outbound-request.ts
|
|
12
|
+
import { isEdgeRuntime } from "../env/browser-env.mjs";
|
|
13
|
+
import { isNode } from "../crypto/runtime.mjs";
|
|
14
|
+
import {
|
|
15
|
+
isAllowedOutboundAddress,
|
|
16
|
+
isIpLiteralHostname,
|
|
17
|
+
validateOutboundUrl
|
|
18
|
+
} from "./outbound-url.mjs";
|
|
19
|
+
var OUTBOUND_TIMEOUT_MS = 1e4;
|
|
20
|
+
var OUTBOUND_MAX_REQUEST_BYTES = 262144;
|
|
21
|
+
var FAILURE_CATEGORIES = {
|
|
22
|
+
invalid_url: "connection_refused",
|
|
23
|
+
forbidden_address: "connection_refused",
|
|
24
|
+
unresolved_host: "connection_refused",
|
|
25
|
+
unpinned_dns: "connection_refused",
|
|
26
|
+
request_too_large: "http_4xx",
|
|
27
|
+
connection_refused: "connection_refused",
|
|
28
|
+
timeout: "timeout",
|
|
29
|
+
tls_error: "tls_error"
|
|
30
|
+
};
|
|
31
|
+
var TIMEOUT_CODE_PATTERN = /TIMEDOUT|TIMEOUT/;
|
|
32
|
+
var TLS_CODE_PATTERN = /CERT|TLS|SSL|CRL|EPROTO|^UNABLE_TO_|^INVALID_(?:CA|PURPOSE)$|^PATH_LENGTH_EXCEEDED$|^HOSTNAME_MISMATCH$/;
|
|
33
|
+
var encoder = new TextEncoder();
|
|
34
|
+
var decoder = new TextDecoder();
|
|
35
|
+
var OutboundRefusal = class extends Error {
|
|
36
|
+
/** Carries the failure reason the request's result reports. */
|
|
37
|
+
constructor(reason) {
|
|
38
|
+
super(reason);
|
|
39
|
+
this.reason = reason;
|
|
40
|
+
}
|
|
41
|
+
reason;
|
|
42
|
+
};
|
|
43
|
+
function outboundFailureCategory(result) {
|
|
44
|
+
if (!result.answered) return FAILURE_CATEGORIES[result.reason];
|
|
45
|
+
if (result.status >= 200 && result.status < 300) return void 0;
|
|
46
|
+
return result.status >= 500 ? "http_5xx" : "http_4xx";
|
|
47
|
+
}
|
|
48
|
+
async function sendOutboundRequest(request) {
|
|
49
|
+
const check = validateOutboundUrl(request.url, request);
|
|
50
|
+
if (!check.ok) return { answered: false, reason: check.problem === "host" ? "forbidden_address" : "invalid_url" };
|
|
51
|
+
const node = loadNodeNetworking();
|
|
52
|
+
const needsPlatformDns = !node && !isIpLiteralHostname(check.url.hostname);
|
|
53
|
+
if (needsPlatformDns && request.allowPlatformDns !== true) return { answered: false, reason: "unpinned_dns" };
|
|
54
|
+
const body = request.body === void 0 ? void 0 : encoder.encode(request.body);
|
|
55
|
+
if (body && body.byteLength > (request.maxRequestBytes ?? OUTBOUND_MAX_REQUEST_BYTES)) {
|
|
56
|
+
return { answered: false, reason: "request_too_large" };
|
|
57
|
+
}
|
|
58
|
+
const timeout = AbortSignal.timeout(request.timeoutMs ?? OUTBOUND_TIMEOUT_MS);
|
|
59
|
+
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
60
|
+
const headers = { ...request.headers };
|
|
61
|
+
if (body) headers["content-length"] = String(body.byteLength);
|
|
62
|
+
const method = request.method ?? (body ? "POST" : "GET");
|
|
63
|
+
const exchange = { url: check.url, method, headers, body, signal };
|
|
64
|
+
try {
|
|
65
|
+
const upstream = node ? await exchangeOverNode(node, exchange, request) : await exchangeOverFetch(exchange);
|
|
66
|
+
return await answerOf(upstream, request);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return { answered: false, reason: failureReasonOf(error, signal) };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function loadNodeNetworking() {
|
|
72
|
+
if (!isNode() || isEdgeRuntime()) return void 0;
|
|
73
|
+
return { dns: __require("node:dns"), http: __require("node:http"), https: __require("node:https") };
|
|
74
|
+
}
|
|
75
|
+
function rejectedOnAbort(signal) {
|
|
76
|
+
return new Promise((_resolve, reject) => {
|
|
77
|
+
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function resolveAllowedAddresses(node, exchange, options) {
|
|
81
|
+
exchange.signal.throwIfAborted();
|
|
82
|
+
const hostname = exchange.url.hostname.replace(/^\[|\]$/g, "");
|
|
83
|
+
let addresses;
|
|
84
|
+
try {
|
|
85
|
+
const lookup = node.dns.promises.lookup(hostname, { all: true });
|
|
86
|
+
addresses = await Promise.race([lookup, rejectedOnAbort(exchange.signal)]);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw exchange.signal.aborted ? error : new OutboundRefusal("unresolved_host");
|
|
89
|
+
}
|
|
90
|
+
if (addresses.length === 0) throw new OutboundRefusal("unresolved_host");
|
|
91
|
+
if (!addresses.every(({ address }) => isAllowedOutboundAddress(address, options))) {
|
|
92
|
+
throw new OutboundRefusal("forbidden_address");
|
|
93
|
+
}
|
|
94
|
+
return addresses;
|
|
95
|
+
}
|
|
96
|
+
function pinnedLookup(addresses) {
|
|
97
|
+
return (_hostname, options, callback) => {
|
|
98
|
+
if (options.all) {
|
|
99
|
+
callback(null, [...addresses]);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const [first] = addresses;
|
|
103
|
+
callback(null, first.address, first.family);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function exchangeOverNode(node, exchange, options) {
|
|
107
|
+
const { url, method, headers, body, signal } = exchange;
|
|
108
|
+
const lookup = pinnedLookup(await resolveAllowedAddresses(node, exchange, options));
|
|
109
|
+
const transport = url.protocol === "https:" ? node.https : node.http;
|
|
110
|
+
const incoming = await new Promise((resolve, reject) => {
|
|
111
|
+
const outgoing = transport.request(url, { method, headers, signal, lookup, agent: false }, resolve);
|
|
112
|
+
outgoing.on("error", reject);
|
|
113
|
+
outgoing.end(body);
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
status: Number(incoming.statusCode),
|
|
117
|
+
headerOf: (name) => {
|
|
118
|
+
const value = incoming.headers[name];
|
|
119
|
+
return value === void 0 ? void 0 : String(value);
|
|
120
|
+
},
|
|
121
|
+
chunks: incoming,
|
|
122
|
+
discard: () => incoming.destroy()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function* chunksOf(stream) {
|
|
126
|
+
const reader = stream.getReader();
|
|
127
|
+
try {
|
|
128
|
+
for (; ; ) {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done) return;
|
|
131
|
+
yield value;
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
await reader.cancel();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function exchangeOverFetch(exchange) {
|
|
138
|
+
const { url, method, headers, body, signal } = exchange;
|
|
139
|
+
const response = await fetch(url, { method, headers, body, redirect: "manual", signal });
|
|
140
|
+
return {
|
|
141
|
+
status: response.status,
|
|
142
|
+
headerOf: (name) => response.headers.get(name) ?? void 0,
|
|
143
|
+
chunks: response.body ? chunksOf(response.body) : [],
|
|
144
|
+
discard: () => response.body?.cancel()
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function readBody(upstream, maxBytes) {
|
|
148
|
+
if (maxBytes <= 0) {
|
|
149
|
+
await upstream.discard();
|
|
150
|
+
return { body: "", bodyTruncated: false };
|
|
151
|
+
}
|
|
152
|
+
const chunks = [];
|
|
153
|
+
let received = 0;
|
|
154
|
+
for await (const chunk of upstream.chunks) {
|
|
155
|
+
chunks.push(chunk);
|
|
156
|
+
received += chunk.byteLength;
|
|
157
|
+
if (received > maxBytes) break;
|
|
158
|
+
}
|
|
159
|
+
const bytes = new Uint8Array(Math.min(received, maxBytes));
|
|
160
|
+
let offset = 0;
|
|
161
|
+
for (const chunk of chunks) {
|
|
162
|
+
bytes.set(chunk.subarray(0, bytes.byteLength - offset), offset);
|
|
163
|
+
offset += chunk.byteLength;
|
|
164
|
+
}
|
|
165
|
+
return { body: decoder.decode(bytes), bodyTruncated: received > maxBytes };
|
|
166
|
+
}
|
|
167
|
+
async function answerOf(upstream, request) {
|
|
168
|
+
const headers = {};
|
|
169
|
+
for (const requestedName of request.responseHeaders ?? []) {
|
|
170
|
+
const name = requestedName.toLowerCase();
|
|
171
|
+
const value = upstream.headerOf(name);
|
|
172
|
+
if (value !== void 0) headers[name] = value;
|
|
173
|
+
}
|
|
174
|
+
const { body, bodyTruncated } = await readBody(upstream, request.maxResponseBytes ?? 0);
|
|
175
|
+
return { answered: true, status: upstream.status, body, bodyTruncated, headers };
|
|
176
|
+
}
|
|
177
|
+
function failureReasonOf(error, signal) {
|
|
178
|
+
if (error instanceof OutboundRefusal) return error.reason;
|
|
179
|
+
if (signal.aborted) return "timeout";
|
|
180
|
+
const { code, cause } = Object(error);
|
|
181
|
+
const errorCode = String(code ?? Object(cause).code);
|
|
182
|
+
if (TIMEOUT_CODE_PATTERN.test(errorCode)) return "timeout";
|
|
183
|
+
return TLS_CODE_PATTERN.test(errorCode) ? "tls_error" : "connection_refused";
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
OUTBOUND_MAX_REQUEST_BYTES,
|
|
187
|
+
OUTBOUND_TIMEOUT_MS,
|
|
188
|
+
outboundFailureCategory,
|
|
189
|
+
sendOutboundRequest
|
|
190
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// libs/utils/src/net/outbound-url.ts
|
|
2
|
+
import { classifyIpAddress, isGloballyRoutableIp, parseIpv4 } from "./ip-address.mjs";
|
|
3
|
+
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain"]);
|
|
4
|
+
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
5
|
+
"metadata",
|
|
6
|
+
"metadata.google.internal",
|
|
7
|
+
"metadata.goog",
|
|
8
|
+
"metadata.azure.com",
|
|
9
|
+
"metadata.aws.com"
|
|
10
|
+
]);
|
|
11
|
+
function isAllowedOutboundAddress(address, options = {}) {
|
|
12
|
+
if (isGloballyRoutableIp(address)) return true;
|
|
13
|
+
const range = classifyIpAddress(address)?.range;
|
|
14
|
+
return options.allowLocal === true && (range === "loopback" || range === "private");
|
|
15
|
+
}
|
|
16
|
+
function isIpLiteralHostname(hostname) {
|
|
17
|
+
return hostname.startsWith("[") || parseIpv4(hostname) !== void 0;
|
|
18
|
+
}
|
|
19
|
+
function isAllowedHostname(hostname, options) {
|
|
20
|
+
if (isIpLiteralHostname(hostname)) return isAllowedOutboundAddress(hostname, options);
|
|
21
|
+
const name = hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
|
|
22
|
+
if (METADATA_HOSTNAMES.has(name)) return false;
|
|
23
|
+
const isLoopbackName = LOOPBACK_HOSTNAMES.has(name) || name.endsWith(".localhost");
|
|
24
|
+
return !isLoopbackName || options.allowLocal === true;
|
|
25
|
+
}
|
|
26
|
+
function validateOutboundUrl(target, options = {}) {
|
|
27
|
+
let url;
|
|
28
|
+
try {
|
|
29
|
+
url = new URL(target);
|
|
30
|
+
} catch {
|
|
31
|
+
return { ok: false, problem: "malformed" };
|
|
32
|
+
}
|
|
33
|
+
const isAllowedScheme = url.protocol === "https:" || url.protocol === "http:" && options.allowLocal === true;
|
|
34
|
+
if (!isAllowedScheme) return { ok: false, problem: "scheme" };
|
|
35
|
+
if (url.username !== "" || url.password !== "") return { ok: false, problem: "credentials" };
|
|
36
|
+
if (!isAllowedHostname(url.hostname, options)) return { ok: false, problem: "host" };
|
|
37
|
+
return { ok: true, url };
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
isAllowedOutboundAddress,
|
|
41
|
+
isIpLiteralHostname,
|
|
42
|
+
validateOutboundUrl
|
|
43
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// libs/utils/src/serialization/canonical-json.ts
|
|
2
|
+
function canonicalJson(value) {
|
|
3
|
+
return serialize(value, /* @__PURE__ */ new Set());
|
|
4
|
+
}
|
|
5
|
+
function refuse(description) {
|
|
6
|
+
throw new TypeError(`canonicalJson: ${description} is not a JSON value`);
|
|
7
|
+
}
|
|
8
|
+
function serialize(value, ancestors) {
|
|
9
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
10
|
+
if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value) : refuse(String(value));
|
|
11
|
+
if (typeof value !== "object") return refuse(typeof value);
|
|
12
|
+
if (ancestors.has(value)) return refuse("a circular reference");
|
|
13
|
+
ancestors.add(value);
|
|
14
|
+
const text = serializeObject(value, ancestors);
|
|
15
|
+
ancestors.delete(value);
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
function serializeObject(value, ancestors) {
|
|
19
|
+
if (hasToJson(value)) return serialize(value.toJSON(), ancestors);
|
|
20
|
+
if (Array.isArray(value)) return `[${Array.from(value, (item) => serialize(item, ancestors)).join(",")}]`;
|
|
21
|
+
if (!isPlainObject(value)) return refuse("a class instance");
|
|
22
|
+
const record = value;
|
|
23
|
+
const members = Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${serialize(record[key], ancestors)}`);
|
|
24
|
+
return `{${members.join(",")}}`;
|
|
25
|
+
}
|
|
26
|
+
function hasToJson(value) {
|
|
27
|
+
return typeof value.toJSON === "function";
|
|
28
|
+
}
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
const prototype = Object.getPrototypeOf(value);
|
|
31
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
canonicalJson
|
|
35
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// libs/utils/src/stdio-bridge/claude-code-channels.ts
|
|
2
|
+
var PROTOCOL_2026_07_28 = "2026-07-28";
|
|
3
|
+
var PROTOCOL_VERSION_META = "io.modelcontextprotocol/protocolVersion";
|
|
4
|
+
var DISCOVER_METHOD = "server/discover";
|
|
5
|
+
var PROTOCOL_20260728_ONLY_METHODS = [DISCOVER_METHOD, "subscriptions/listen"];
|
|
6
|
+
var UNSUPPORTED_PROTOCOL_VERSION = -32022;
|
|
7
|
+
var CHANNEL_CAPABILITY = "claude/channel";
|
|
8
|
+
var DISCOVER_HEADERS = { "mcp-protocol-version": PROTOCOL_2026_07_28, "mcp-method": DISCOVER_METHOD };
|
|
9
|
+
function recordOf(value) {
|
|
10
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
11
|
+
}
|
|
12
|
+
function declaredRevision(frame) {
|
|
13
|
+
const declared = recordOf(recordOf(frame.params)?.["_meta"])?.[PROTOCOL_VERSION_META];
|
|
14
|
+
return typeof declared === "string" ? declared : void 0;
|
|
15
|
+
}
|
|
16
|
+
function namesProtocol20260728(frame) {
|
|
17
|
+
if (frame.method === void 0) return false;
|
|
18
|
+
return declaredRevision(frame) !== void 0 || PROTOCOL_20260728_ONLY_METHODS.includes(frame.method);
|
|
19
|
+
}
|
|
20
|
+
function unsupportedProtocolVersion(request, supported) {
|
|
21
|
+
const requested = declaredRevision(request) ?? PROTOCOL_2026_07_28;
|
|
22
|
+
return {
|
|
23
|
+
jsonrpc: "2.0",
|
|
24
|
+
id: request.id,
|
|
25
|
+
error: {
|
|
26
|
+
code: UNSUPPORTED_PROTOCOL_VERSION,
|
|
27
|
+
message: `Unsupported protocol version: ${requested}`,
|
|
28
|
+
data: { supported: [...supported], requested }
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function discoverRequest(id) {
|
|
33
|
+
return {
|
|
34
|
+
jsonrpc: "2.0",
|
|
35
|
+
id,
|
|
36
|
+
method: DISCOVER_METHOD,
|
|
37
|
+
params: { _meta: { [PROTOCOL_VERSION_META]: PROTOCOL_2026_07_28 } }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function advertisesChannels(result) {
|
|
41
|
+
const experimental = recordOf(recordOf(recordOf(result)?.["capabilities"])?.["experimental"]);
|
|
42
|
+
return experimental?.[CHANNEL_CAPABILITY] !== void 0;
|
|
43
|
+
}
|
|
44
|
+
function serverRevisionsOf(discoverResult) {
|
|
45
|
+
const supportedVersions = recordOf(discoverResult)?.["supportedVersions"];
|
|
46
|
+
if (!Array.isArray(supportedVersions)) return void 0;
|
|
47
|
+
const supported = supportedVersions.filter(
|
|
48
|
+
(version) => typeof version === "string" && version !== PROTOCOL_2026_07_28
|
|
49
|
+
);
|
|
50
|
+
return { pushesChannels: advertisesChannels(discoverResult), supported };
|
|
51
|
+
}
|
|
52
|
+
function declaringChannels(initialize) {
|
|
53
|
+
const params = recordOf(initialize.params) ?? {};
|
|
54
|
+
const capabilities = recordOf(params["capabilities"]) ?? {};
|
|
55
|
+
const experimental = recordOf(capabilities["experimental"]) ?? {};
|
|
56
|
+
if (experimental[CHANNEL_CAPABILITY] !== void 0) return initialize;
|
|
57
|
+
return {
|
|
58
|
+
...initialize,
|
|
59
|
+
params: {
|
|
60
|
+
...params,
|
|
61
|
+
capabilities: { ...capabilities, experimental: { ...experimental, [CHANNEL_CAPABILITY]: {} } }
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
DISCOVER_HEADERS,
|
|
67
|
+
advertisesChannels,
|
|
68
|
+
declaringChannels,
|
|
69
|
+
discoverRequest,
|
|
70
|
+
namesProtocol20260728,
|
|
71
|
+
serverRevisionsOf,
|
|
72
|
+
unsupportedProtocolVersion
|
|
73
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// libs/utils/src/stdio-bridge/notification-stream.ts
|
|
2
|
+
import { relaySseFrames } from "./sse-frames.mjs";
|
|
3
|
+
var SESSION_HEADER = "mcp-session-id";
|
|
4
|
+
var NO_STREAM_STATUS = 405;
|
|
5
|
+
var UNKNOWN_SESSION_STATUS = 404;
|
|
6
|
+
var OPEN_WAIT_MS = 2e3;
|
|
7
|
+
var FIRST_REOPEN_DELAY_MS = 250;
|
|
8
|
+
var MAX_REOPEN_DELAY_MS = 4e3;
|
|
9
|
+
var MAX_FAILED_OPENS = 5;
|
|
10
|
+
function pause(ms, signal) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const timer = setTimeout(finish, ms).unref();
|
|
13
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
14
|
+
function finish() {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
signal.removeEventListener("abort", finish);
|
|
17
|
+
resolve();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function createNotificationStream(options) {
|
|
22
|
+
const { url, log, onFrame } = options;
|
|
23
|
+
let following;
|
|
24
|
+
async function open(sessionId, signal) {
|
|
25
|
+
try {
|
|
26
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
27
|
+
method: "GET",
|
|
28
|
+
redirect: "error",
|
|
29
|
+
headers: { ...options.headers, accept: "text/event-stream", [SESSION_HEADER]: sessionId },
|
|
30
|
+
signal
|
|
31
|
+
});
|
|
32
|
+
const streams = (response.headers.get("content-type") ?? "").includes("text/event-stream");
|
|
33
|
+
if (response.ok && streams && response.body) return response.body;
|
|
34
|
+
await response.body?.cancel();
|
|
35
|
+
const unknownSession = response.status === UNKNOWN_SESSION_STATUS;
|
|
36
|
+
const offersNone = unknownSession || response.ok || response.status === NO_STREAM_STATUS;
|
|
37
|
+
log.debug(offersNone ? "notification-stream-unavailable" : "notification-stream-failed", {
|
|
38
|
+
status: response.status
|
|
39
|
+
});
|
|
40
|
+
if (unknownSession) return "unknown-session";
|
|
41
|
+
return offersNone ? "unavailable" : "failed";
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (!signal.aborted) log.debug("notification-stream-failed", { error: err.message });
|
|
44
|
+
return "failed";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function relay(body, signal) {
|
|
48
|
+
let relayed = false;
|
|
49
|
+
try {
|
|
50
|
+
await relaySseFrames(
|
|
51
|
+
body,
|
|
52
|
+
async (frame) => {
|
|
53
|
+
relayed = true;
|
|
54
|
+
await onFrame(frame);
|
|
55
|
+
},
|
|
56
|
+
log
|
|
57
|
+
);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (!signal.aborted) log.debug("notification-stream-dropped", { error: err.message });
|
|
60
|
+
}
|
|
61
|
+
return relayed;
|
|
62
|
+
}
|
|
63
|
+
function giveUp(reason) {
|
|
64
|
+
log.warn("notification-stream-lost", reason);
|
|
65
|
+
options.onLost?.();
|
|
66
|
+
}
|
|
67
|
+
async function relayUntilEnded(sessionId, signal, onAnswered) {
|
|
68
|
+
let failedOpens = 0;
|
|
69
|
+
let streamed = false;
|
|
70
|
+
let delayMs = FIRST_REOPEN_DELAY_MS;
|
|
71
|
+
while (!signal.aborted) {
|
|
72
|
+
const opened = await open(sessionId, signal);
|
|
73
|
+
onAnswered();
|
|
74
|
+
if (signal.aborted || opened === "unavailable") return;
|
|
75
|
+
if (opened === "unknown-session") {
|
|
76
|
+
if (streamed) giveUp({ status: UNKNOWN_SESSION_STATUS });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (opened === "failed") {
|
|
80
|
+
failedOpens += 1;
|
|
81
|
+
if (failedOpens === MAX_FAILED_OPENS) {
|
|
82
|
+
giveUp({ attempts: failedOpens });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
failedOpens = 0;
|
|
87
|
+
streamed = true;
|
|
88
|
+
if (await relay(opened, signal)) delayMs = FIRST_REOPEN_DELAY_MS;
|
|
89
|
+
}
|
|
90
|
+
await pause(delayMs, signal);
|
|
91
|
+
delayMs = Math.min(delayMs * 2, MAX_REOPEN_DELAY_MS);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
follow(sessionId) {
|
|
96
|
+
following?.abort();
|
|
97
|
+
following = void 0;
|
|
98
|
+
if (!sessionId) return Promise.resolve();
|
|
99
|
+
const stream = new AbortController();
|
|
100
|
+
following = stream;
|
|
101
|
+
return new Promise((answered) => {
|
|
102
|
+
void relayUntilEnded(sessionId, stream.signal, answered);
|
|
103
|
+
void pause(OPEN_WAIT_MS, stream.signal).then(answered);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
SESSION_HEADER,
|
|
110
|
+
createNotificationStream
|
|
111
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// libs/utils/src/stdio-bridge/sse-frames.ts
|
|
2
|
+
var MAX_EVENT_CHARS = 64 * 1024 * 1024;
|
|
3
|
+
async function relaySseFrames(body, deliver, log, maxEventChars = MAX_EVENT_CHARS) {
|
|
4
|
+
const reader = body.getReader();
|
|
5
|
+
const decoder = new TextDecoder();
|
|
6
|
+
let buffer = "";
|
|
7
|
+
let eventData = [];
|
|
8
|
+
let eventChars = 0;
|
|
9
|
+
async function deliverEvent() {
|
|
10
|
+
const json = eventData.join("\n").trim();
|
|
11
|
+
eventData = [];
|
|
12
|
+
eventChars = 0;
|
|
13
|
+
if (!json) return;
|
|
14
|
+
try {
|
|
15
|
+
await deliver(JSON.parse(json));
|
|
16
|
+
} catch (err) {
|
|
17
|
+
log.warn("sse-parse-error", { error: err.message, raw: json.slice(0, 200) });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
while (true) {
|
|
21
|
+
const { done, value } = await reader.read();
|
|
22
|
+
if (done) {
|
|
23
|
+
await deliverEvent();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
buffer += decoder.decode(value, { stream: true });
|
|
27
|
+
let newline = buffer.indexOf("\n");
|
|
28
|
+
while (newline >= 0) {
|
|
29
|
+
const line = buffer.slice(0, newline).trim();
|
|
30
|
+
buffer = buffer.slice(newline + 1);
|
|
31
|
+
newline = buffer.indexOf("\n");
|
|
32
|
+
if (line === "") {
|
|
33
|
+
await deliverEvent();
|
|
34
|
+
} else if (line.startsWith("data:")) {
|
|
35
|
+
const data = line.slice(5).trim();
|
|
36
|
+
eventData.push(data);
|
|
37
|
+
eventChars += data.length + 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (eventChars + buffer.length > maxEventChars) {
|
|
41
|
+
await reader.cancel();
|
|
42
|
+
throw new Error(`an SSE event is larger than ${maxEventChars} characters`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
relaySseFrames
|
|
48
|
+
};
|