@chrrxs/robloxstudio-mcp-inspector 3.0.2 → 3.0.4

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/dist/index.js CHANGED
@@ -1,4 +1,1937 @@
1
1
  #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+
28
+ // ../../node_modules/xmlchars/xml/1.0/ed5.js
29
+ var require_ed5 = __commonJS({
30
+ "../../node_modules/xmlchars/xml/1.0/ed5.js"(exports) {
31
+ "use strict";
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.CHAR = " \n\r -\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}";
34
+ exports.S = " \r\n";
35
+ exports.NAME_START_CHAR = ":A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}";
36
+ exports.NAME_CHAR = "-" + exports.NAME_START_CHAR + ".0-9\xB7\u0300-\u036F\u203F-\u2040";
37
+ exports.CHAR_RE = new RegExp("^[" + exports.CHAR + "]$", "u");
38
+ exports.S_RE = new RegExp("^[" + exports.S + "]+$", "u");
39
+ exports.NAME_START_CHAR_RE = new RegExp("^[" + exports.NAME_START_CHAR + "]$", "u");
40
+ exports.NAME_CHAR_RE = new RegExp("^[" + exports.NAME_CHAR + "]$", "u");
41
+ exports.NAME_RE = new RegExp("^[" + exports.NAME_START_CHAR + "][" + exports.NAME_CHAR + "]*$", "u");
42
+ exports.NMTOKEN_RE = new RegExp("^[" + exports.NAME_CHAR + "]+$", "u");
43
+ var TAB = 9;
44
+ var NL = 10;
45
+ var CR = 13;
46
+ var SPACE = 32;
47
+ exports.S_LIST = [SPACE, NL, CR, TAB];
48
+ function isChar(c) {
49
+ return c >= SPACE && c <= 55295 || c === NL || c === CR || c === TAB || c >= 57344 && c <= 65533 || c >= 65536 && c <= 1114111;
50
+ }
51
+ exports.isChar = isChar;
52
+ function isS(c) {
53
+ return c === SPACE || c === NL || c === CR || c === TAB;
54
+ }
55
+ exports.isS = isS;
56
+ function isNameStartChar(c) {
57
+ return c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 58 || c === 95 || c === 8204 || c === 8205 || c >= 192 && c <= 214 || c >= 216 && c <= 246 || c >= 248 && c <= 767 || c >= 880 && c <= 893 || c >= 895 && c <= 8191 || c >= 8304 && c <= 8591 || c >= 11264 && c <= 12271 || c >= 12289 && c <= 55295 || c >= 63744 && c <= 64975 || c >= 65008 && c <= 65533 || c >= 65536 && c <= 983039;
58
+ }
59
+ exports.isNameStartChar = isNameStartChar;
60
+ function isNameChar(c) {
61
+ return isNameStartChar(c) || c >= 48 && c <= 57 || c === 45 || c === 46 || c === 183 || c >= 768 && c <= 879 || c >= 8255 && c <= 8256;
62
+ }
63
+ exports.isNameChar = isNameChar;
64
+ }
65
+ });
66
+
67
+ // ../../node_modules/xmlchars/xml/1.1/ed2.js
68
+ var require_ed2 = __commonJS({
69
+ "../../node_modules/xmlchars/xml/1.1/ed2.js"(exports) {
70
+ "use strict";
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.CHAR = "-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}";
73
+ exports.RESTRICTED_CHAR = "-\b\v\f-\x7F-\x84\x86-\x9F";
74
+ exports.S = " \r\n";
75
+ exports.NAME_START_CHAR = ":A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}";
76
+ exports.NAME_CHAR = "-" + exports.NAME_START_CHAR + ".0-9\xB7\u0300-\u036F\u203F-\u2040";
77
+ exports.CHAR_RE = new RegExp("^[" + exports.CHAR + "]$", "u");
78
+ exports.RESTRICTED_CHAR_RE = new RegExp("^[" + exports.RESTRICTED_CHAR + "]$", "u");
79
+ exports.S_RE = new RegExp("^[" + exports.S + "]+$", "u");
80
+ exports.NAME_START_CHAR_RE = new RegExp("^[" + exports.NAME_START_CHAR + "]$", "u");
81
+ exports.NAME_CHAR_RE = new RegExp("^[" + exports.NAME_CHAR + "]$", "u");
82
+ exports.NAME_RE = new RegExp("^[" + exports.NAME_START_CHAR + "][" + exports.NAME_CHAR + "]*$", "u");
83
+ exports.NMTOKEN_RE = new RegExp("^[" + exports.NAME_CHAR + "]+$", "u");
84
+ var TAB = 9;
85
+ var NL = 10;
86
+ var CR = 13;
87
+ var SPACE = 32;
88
+ exports.S_LIST = [SPACE, NL, CR, TAB];
89
+ function isChar(c) {
90
+ return c >= 1 && c <= 55295 || c >= 57344 && c <= 65533 || c >= 65536 && c <= 1114111;
91
+ }
92
+ exports.isChar = isChar;
93
+ function isRestrictedChar(c) {
94
+ return c >= 1 && c <= 8 || c === 11 || c === 12 || c >= 14 && c <= 31 || c >= 127 && c <= 132 || c >= 134 && c <= 159;
95
+ }
96
+ exports.isRestrictedChar = isRestrictedChar;
97
+ function isCharAndNotRestricted(c) {
98
+ return c === 9 || c === 10 || c === 13 || c > 31 && c < 127 || c === 133 || c > 159 && c <= 55295 || c >= 57344 && c <= 65533 || c >= 65536 && c <= 1114111;
99
+ }
100
+ exports.isCharAndNotRestricted = isCharAndNotRestricted;
101
+ function isS(c) {
102
+ return c === SPACE || c === NL || c === CR || c === TAB;
103
+ }
104
+ exports.isS = isS;
105
+ function isNameStartChar(c) {
106
+ return c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 58 || c === 95 || c === 8204 || c === 8205 || c >= 192 && c <= 214 || c >= 216 && c <= 246 || c >= 248 && c <= 767 || c >= 880 && c <= 893 || c >= 895 && c <= 8191 || c >= 8304 && c <= 8591 || c >= 11264 && c <= 12271 || c >= 12289 && c <= 55295 || c >= 63744 && c <= 64975 || c >= 65008 && c <= 65533 || c >= 65536 && c <= 983039;
107
+ }
108
+ exports.isNameStartChar = isNameStartChar;
109
+ function isNameChar(c) {
110
+ return isNameStartChar(c) || c >= 48 && c <= 57 || c === 45 || c === 46 || c === 183 || c >= 768 && c <= 879 || c >= 8255 && c <= 8256;
111
+ }
112
+ exports.isNameChar = isNameChar;
113
+ }
114
+ });
115
+
116
+ // ../../node_modules/xmlchars/xmlns/1.0/ed3.js
117
+ var require_ed3 = __commonJS({
118
+ "../../node_modules/xmlchars/xmlns/1.0/ed3.js"(exports) {
119
+ "use strict";
120
+ Object.defineProperty(exports, "__esModule", { value: true });
121
+ exports.NC_NAME_START_CHAR = "A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}";
122
+ exports.NC_NAME_CHAR = "-" + exports.NC_NAME_START_CHAR + ".0-9\xB7\u0300-\u036F\u203F-\u2040";
123
+ exports.NC_NAME_START_CHAR_RE = new RegExp("^[" + exports.NC_NAME_START_CHAR + "]$", "u");
124
+ exports.NC_NAME_CHAR_RE = new RegExp("^[" + exports.NC_NAME_CHAR + "]$", "u");
125
+ exports.NC_NAME_RE = new RegExp("^[" + exports.NC_NAME_START_CHAR + "][" + exports.NC_NAME_CHAR + "]*$", "u");
126
+ function isNCNameStartChar(c) {
127
+ return c >= 65 && c <= 90 || c === 95 || c >= 97 && c <= 122 || c >= 192 && c <= 214 || c >= 216 && c <= 246 || c >= 248 && c <= 767 || c >= 880 && c <= 893 || c >= 895 && c <= 8191 || c >= 8204 && c <= 8205 || c >= 8304 && c <= 8591 || c >= 11264 && c <= 12271 || c >= 12289 && c <= 55295 || c >= 63744 && c <= 64975 || c >= 65008 && c <= 65533 || c >= 65536 && c <= 983039;
128
+ }
129
+ exports.isNCNameStartChar = isNCNameStartChar;
130
+ function isNCNameChar(c) {
131
+ return isNCNameStartChar(c) || (c === 45 || c === 46 || c >= 48 && c <= 57 || c === 183 || c >= 768 && c <= 879 || c >= 8255 && c <= 8256);
132
+ }
133
+ exports.isNCNameChar = isNCNameChar;
134
+ }
135
+ });
136
+
137
+ // ../../node_modules/saxes/saxes.js
138
+ var require_saxes = __commonJS({
139
+ "../../node_modules/saxes/saxes.js"(exports) {
140
+ "use strict";
141
+ Object.defineProperty(exports, "__esModule", { value: true });
142
+ exports.SaxesParser = exports.EVENTS = void 0;
143
+ var ed5 = require_ed5();
144
+ var ed2 = require_ed2();
145
+ var NSed3 = require_ed3();
146
+ var isS = ed5.isS;
147
+ var isChar10 = ed5.isChar;
148
+ var isNameStartChar = ed5.isNameStartChar;
149
+ var isNameChar = ed5.isNameChar;
150
+ var S_LIST = ed5.S_LIST;
151
+ var NAME_RE = ed5.NAME_RE;
152
+ var isChar11 = ed2.isChar;
153
+ var isNCNameStartChar = NSed3.isNCNameStartChar;
154
+ var isNCNameChar = NSed3.isNCNameChar;
155
+ var NC_NAME_RE = NSed3.NC_NAME_RE;
156
+ var XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
157
+ var XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
158
+ var rootNS = {
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
160
+ __proto__: null,
161
+ xml: XML_NAMESPACE,
162
+ xmlns: XMLNS_NAMESPACE
163
+ };
164
+ var XML_ENTITIES = {
165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
166
+ __proto__: null,
167
+ amp: "&",
168
+ gt: ">",
169
+ lt: "<",
170
+ quot: '"',
171
+ apos: "'"
172
+ };
173
+ var EOC = -1;
174
+ var NL_LIKE = -2;
175
+ var S_BEGIN = 0;
176
+ var S_BEGIN_WHITESPACE = 1;
177
+ var S_DOCTYPE = 2;
178
+ var S_DOCTYPE_QUOTE = 3;
179
+ var S_DTD = 4;
180
+ var S_DTD_QUOTED = 5;
181
+ var S_DTD_OPEN_WAKA = 6;
182
+ var S_DTD_OPEN_WAKA_BANG = 7;
183
+ var S_DTD_COMMENT = 8;
184
+ var S_DTD_COMMENT_ENDING = 9;
185
+ var S_DTD_COMMENT_ENDED = 10;
186
+ var S_DTD_PI = 11;
187
+ var S_DTD_PI_ENDING = 12;
188
+ var S_TEXT = 13;
189
+ var S_ENTITY = 14;
190
+ var S_OPEN_WAKA = 15;
191
+ var S_OPEN_WAKA_BANG = 16;
192
+ var S_COMMENT = 17;
193
+ var S_COMMENT_ENDING = 18;
194
+ var S_COMMENT_ENDED = 19;
195
+ var S_CDATA = 20;
196
+ var S_CDATA_ENDING = 21;
197
+ var S_CDATA_ENDING_2 = 22;
198
+ var S_PI_FIRST_CHAR = 23;
199
+ var S_PI_REST = 24;
200
+ var S_PI_BODY = 25;
201
+ var S_PI_ENDING = 26;
202
+ var S_XML_DECL_NAME_START = 27;
203
+ var S_XML_DECL_NAME = 28;
204
+ var S_XML_DECL_EQ = 29;
205
+ var S_XML_DECL_VALUE_START = 30;
206
+ var S_XML_DECL_VALUE = 31;
207
+ var S_XML_DECL_SEPARATOR = 32;
208
+ var S_XML_DECL_ENDING = 33;
209
+ var S_OPEN_TAG = 34;
210
+ var S_OPEN_TAG_SLASH = 35;
211
+ var S_ATTRIB = 36;
212
+ var S_ATTRIB_NAME = 37;
213
+ var S_ATTRIB_NAME_SAW_WHITE = 38;
214
+ var S_ATTRIB_VALUE = 39;
215
+ var S_ATTRIB_VALUE_QUOTED = 40;
216
+ var S_ATTRIB_VALUE_CLOSED = 41;
217
+ var S_ATTRIB_VALUE_UNQUOTED = 42;
218
+ var S_CLOSE_TAG = 43;
219
+ var S_CLOSE_TAG_SAW_WHITE = 44;
220
+ var TAB = 9;
221
+ var NL = 10;
222
+ var CR = 13;
223
+ var SPACE = 32;
224
+ var BANG = 33;
225
+ var DQUOTE = 34;
226
+ var AMP = 38;
227
+ var SQUOTE = 39;
228
+ var MINUS = 45;
229
+ var FORWARD_SLASH = 47;
230
+ var SEMICOLON = 59;
231
+ var LESS = 60;
232
+ var EQUAL = 61;
233
+ var GREATER = 62;
234
+ var QUESTION = 63;
235
+ var OPEN_BRACKET = 91;
236
+ var CLOSE_BRACKET = 93;
237
+ var NEL = 133;
238
+ var LS = 8232;
239
+ var isQuote = (c) => c === DQUOTE || c === SQUOTE;
240
+ var QUOTES = [DQUOTE, SQUOTE];
241
+ var DOCTYPE_TERMINATOR = [...QUOTES, OPEN_BRACKET, GREATER];
242
+ var DTD_TERMINATOR = [...QUOTES, LESS, CLOSE_BRACKET];
243
+ var XML_DECL_NAME_TERMINATOR = [EQUAL, QUESTION, ...S_LIST];
244
+ var ATTRIB_VALUE_UNQUOTED_TERMINATOR = [...S_LIST, GREATER, AMP, LESS];
245
+ function nsPairCheck(parser, prefix, uri) {
246
+ switch (prefix) {
247
+ case "xml":
248
+ if (uri !== XML_NAMESPACE) {
249
+ parser.fail(`xml prefix must be bound to ${XML_NAMESPACE}.`);
250
+ }
251
+ break;
252
+ case "xmlns":
253
+ if (uri !== XMLNS_NAMESPACE) {
254
+ parser.fail(`xmlns prefix must be bound to ${XMLNS_NAMESPACE}.`);
255
+ }
256
+ break;
257
+ default:
258
+ }
259
+ switch (uri) {
260
+ case XMLNS_NAMESPACE:
261
+ parser.fail(prefix === "" ? `the default namespace may not be set to ${uri}.` : `may not assign a prefix (even "xmlns") to the URI ${XMLNS_NAMESPACE}.`);
262
+ break;
263
+ case XML_NAMESPACE:
264
+ switch (prefix) {
265
+ case "xml":
266
+ break;
267
+ case "":
268
+ parser.fail(`the default namespace may not be set to ${uri}.`);
269
+ break;
270
+ default:
271
+ parser.fail("may not assign the xml namespace to another prefix.");
272
+ }
273
+ break;
274
+ default:
275
+ }
276
+ }
277
+ function nsMappingCheck(parser, mapping) {
278
+ for (const local of Object.keys(mapping)) {
279
+ nsPairCheck(parser, local, mapping[local]);
280
+ }
281
+ }
282
+ var isNCName = (name) => NC_NAME_RE.test(name);
283
+ var isName = (name) => NAME_RE.test(name);
284
+ var FORBIDDEN_START = 0;
285
+ var FORBIDDEN_BRACKET = 1;
286
+ var FORBIDDEN_BRACKET_BRACKET = 2;
287
+ exports.EVENTS = [
288
+ "xmldecl",
289
+ "text",
290
+ "processinginstruction",
291
+ "doctype",
292
+ "comment",
293
+ "opentagstart",
294
+ "attribute",
295
+ "opentag",
296
+ "closetag",
297
+ "cdata",
298
+ "error",
299
+ "end",
300
+ "ready"
301
+ ];
302
+ var EVENT_NAME_TO_HANDLER_NAME = {
303
+ xmldecl: "xmldeclHandler",
304
+ text: "textHandler",
305
+ processinginstruction: "piHandler",
306
+ doctype: "doctypeHandler",
307
+ comment: "commentHandler",
308
+ opentagstart: "openTagStartHandler",
309
+ attribute: "attributeHandler",
310
+ opentag: "openTagHandler",
311
+ closetag: "closeTagHandler",
312
+ cdata: "cdataHandler",
313
+ error: "errorHandler",
314
+ end: "endHandler",
315
+ ready: "readyHandler"
316
+ };
317
+ var SaxesParser2 = class {
318
+ /**
319
+ * @param opt The parser options.
320
+ */
321
+ constructor(opt) {
322
+ this.opt = opt !== null && opt !== void 0 ? opt : {};
323
+ this.fragmentOpt = !!this.opt.fragment;
324
+ const xmlnsOpt = this.xmlnsOpt = !!this.opt.xmlns;
325
+ this.trackPosition = this.opt.position !== false;
326
+ this.fileName = this.opt.fileName;
327
+ if (xmlnsOpt) {
328
+ this.nameStartCheck = isNCNameStartChar;
329
+ this.nameCheck = isNCNameChar;
330
+ this.isName = isNCName;
331
+ this.processAttribs = this.processAttribsNS;
332
+ this.pushAttrib = this.pushAttribNS;
333
+ this.ns = Object.assign({ __proto__: null }, rootNS);
334
+ const additional = this.opt.additionalNamespaces;
335
+ if (additional != null) {
336
+ nsMappingCheck(this, additional);
337
+ Object.assign(this.ns, additional);
338
+ }
339
+ } else {
340
+ this.nameStartCheck = isNameStartChar;
341
+ this.nameCheck = isNameChar;
342
+ this.isName = isName;
343
+ this.processAttribs = this.processAttribsPlain;
344
+ this.pushAttrib = this.pushAttribPlain;
345
+ }
346
+ this.stateTable = [
347
+ /* eslint-disable @typescript-eslint/unbound-method */
348
+ this.sBegin,
349
+ this.sBeginWhitespace,
350
+ this.sDoctype,
351
+ this.sDoctypeQuote,
352
+ this.sDTD,
353
+ this.sDTDQuoted,
354
+ this.sDTDOpenWaka,
355
+ this.sDTDOpenWakaBang,
356
+ this.sDTDComment,
357
+ this.sDTDCommentEnding,
358
+ this.sDTDCommentEnded,
359
+ this.sDTDPI,
360
+ this.sDTDPIEnding,
361
+ this.sText,
362
+ this.sEntity,
363
+ this.sOpenWaka,
364
+ this.sOpenWakaBang,
365
+ this.sComment,
366
+ this.sCommentEnding,
367
+ this.sCommentEnded,
368
+ this.sCData,
369
+ this.sCDataEnding,
370
+ this.sCDataEnding2,
371
+ this.sPIFirstChar,
372
+ this.sPIRest,
373
+ this.sPIBody,
374
+ this.sPIEnding,
375
+ this.sXMLDeclNameStart,
376
+ this.sXMLDeclName,
377
+ this.sXMLDeclEq,
378
+ this.sXMLDeclValueStart,
379
+ this.sXMLDeclValue,
380
+ this.sXMLDeclSeparator,
381
+ this.sXMLDeclEnding,
382
+ this.sOpenTag,
383
+ this.sOpenTagSlash,
384
+ this.sAttrib,
385
+ this.sAttribName,
386
+ this.sAttribNameSawWhite,
387
+ this.sAttribValue,
388
+ this.sAttribValueQuoted,
389
+ this.sAttribValueClosed,
390
+ this.sAttribValueUnquoted,
391
+ this.sCloseTag,
392
+ this.sCloseTagSawWhite
393
+ /* eslint-enable @typescript-eslint/unbound-method */
394
+ ];
395
+ this._init();
396
+ }
397
+ /**
398
+ * Indicates whether or not the parser is closed. If ``true``, wait for
399
+ * the ``ready`` event to write again.
400
+ */
401
+ get closed() {
402
+ return this._closed;
403
+ }
404
+ _init() {
405
+ var _a;
406
+ this.openWakaBang = "";
407
+ this.text = "";
408
+ this.name = "";
409
+ this.piTarget = "";
410
+ this.entity = "";
411
+ this.q = null;
412
+ this.tags = [];
413
+ this.tag = null;
414
+ this.topNS = null;
415
+ this.chunk = "";
416
+ this.chunkPosition = 0;
417
+ this.i = 0;
418
+ this.prevI = 0;
419
+ this.carriedFromPrevious = void 0;
420
+ this.forbiddenState = FORBIDDEN_START;
421
+ this.attribList = [];
422
+ const { fragmentOpt } = this;
423
+ this.state = fragmentOpt ? S_TEXT : S_BEGIN;
424
+ this.reportedTextBeforeRoot = this.reportedTextAfterRoot = this.closedRoot = this.sawRoot = fragmentOpt;
425
+ this.xmlDeclPossible = !fragmentOpt;
426
+ this.xmlDeclExpects = ["version"];
427
+ this.entityReturnState = void 0;
428
+ let { defaultXMLVersion } = this.opt;
429
+ if (defaultXMLVersion === void 0) {
430
+ if (this.opt.forceXMLVersion === true) {
431
+ throw new Error("forceXMLVersion set but defaultXMLVersion is not set");
432
+ }
433
+ defaultXMLVersion = "1.0";
434
+ }
435
+ this.setXMLVersion(defaultXMLVersion);
436
+ this.positionAtNewLine = 0;
437
+ this.doctype = false;
438
+ this._closed = false;
439
+ this.xmlDecl = {
440
+ version: void 0,
441
+ encoding: void 0,
442
+ standalone: void 0
443
+ };
444
+ this.line = 1;
445
+ this.column = 0;
446
+ this.ENTITIES = Object.create(XML_ENTITIES);
447
+ (_a = this.readyHandler) === null || _a === void 0 ? void 0 : _a.call(this);
448
+ }
449
+ /**
450
+ * The stream position the parser is currently looking at. This field is
451
+ * zero-based.
452
+ *
453
+ * This field is not based on counting Unicode characters but is to be
454
+ * interpreted as a plain index into a JavaScript string.
455
+ */
456
+ get position() {
457
+ return this.chunkPosition + this.i;
458
+ }
459
+ /**
460
+ * The column number of the next character to be read by the parser. *
461
+ * This field is zero-based. (The first column in a line is 0.)
462
+ *
463
+ * This field reports the index at which the next character would be in the
464
+ * line if the line were represented as a JavaScript string. Note that this
465
+ * *can* be different to a count based on the number of *Unicode characters*
466
+ * due to how JavaScript handles astral plane characters.
467
+ *
468
+ * See [[column]] for a number that corresponds to a count of Unicode
469
+ * characters.
470
+ */
471
+ get columnIndex() {
472
+ return this.position - this.positionAtNewLine;
473
+ }
474
+ /**
475
+ * Set an event listener on an event. The parser supports one handler per
476
+ * event type. If you try to set an event handler over an existing handler,
477
+ * the old handler is silently overwritten.
478
+ *
479
+ * @param name The event to listen to.
480
+ *
481
+ * @param handler The handler to set.
482
+ */
483
+ on(name, handler) {
484
+ this[EVENT_NAME_TO_HANDLER_NAME[name]] = handler;
485
+ }
486
+ /**
487
+ * Unset an event handler.
488
+ *
489
+ * @parma name The event to stop listening to.
490
+ */
491
+ off(name) {
492
+ this[EVENT_NAME_TO_HANDLER_NAME[name]] = void 0;
493
+ }
494
+ /**
495
+ * Make an error object. The error object will have a message that contains
496
+ * the ``fileName`` option passed at the creation of the parser. If position
497
+ * tracking was turned on, it will also have line and column number
498
+ * information.
499
+ *
500
+ * @param message The message describing the error to report.
501
+ *
502
+ * @returns An error object with a properly formatted message.
503
+ */
504
+ makeError(message) {
505
+ var _a;
506
+ let msg = (_a = this.fileName) !== null && _a !== void 0 ? _a : "";
507
+ if (this.trackPosition) {
508
+ if (msg.length > 0) {
509
+ msg += ":";
510
+ }
511
+ msg += `${this.line}:${this.column}`;
512
+ }
513
+ if (msg.length > 0) {
514
+ msg += ": ";
515
+ }
516
+ return new Error(msg + message);
517
+ }
518
+ /**
519
+ * Report a parsing error. This method is made public so that client code may
520
+ * check for issues that are outside the scope of this project and can report
521
+ * errors.
522
+ *
523
+ * @param message The error to report.
524
+ *
525
+ * @returns this
526
+ */
527
+ fail(message) {
528
+ const err2 = this.makeError(message);
529
+ const handler = this.errorHandler;
530
+ if (handler === void 0) {
531
+ throw err2;
532
+ } else {
533
+ handler(err2);
534
+ }
535
+ return this;
536
+ }
537
+ /**
538
+ * Write a XML data to the parser.
539
+ *
540
+ * @param chunk The XML data to write.
541
+ *
542
+ * @returns this
543
+ */
544
+ // We do need object for the type here. Yes, it often causes problems
545
+ // but not in this case.
546
+ write(chunk) {
547
+ if (this.closed) {
548
+ return this.fail("cannot write after close; assign an onready handler.");
549
+ }
550
+ let end = false;
551
+ if (chunk === null) {
552
+ end = true;
553
+ chunk = "";
554
+ } else if (typeof chunk === "object") {
555
+ chunk = chunk.toString();
556
+ }
557
+ if (this.carriedFromPrevious !== void 0) {
558
+ chunk = `${this.carriedFromPrevious}${chunk}`;
559
+ this.carriedFromPrevious = void 0;
560
+ }
561
+ let limit = chunk.length;
562
+ const lastCode = chunk.charCodeAt(limit - 1);
563
+ if (!end && // A trailing CR or surrogate must be carried over to the next
564
+ // chunk.
565
+ (lastCode === CR || lastCode >= 55296 && lastCode <= 56319)) {
566
+ this.carriedFromPrevious = chunk[limit - 1];
567
+ limit--;
568
+ chunk = chunk.slice(0, limit);
569
+ }
570
+ const { stateTable } = this;
571
+ this.chunk = chunk;
572
+ this.i = 0;
573
+ while (this.i < limit) {
574
+ stateTable[this.state].call(this);
575
+ }
576
+ this.chunkPosition += limit;
577
+ return end ? this.end() : this;
578
+ }
579
+ /**
580
+ * Close the current stream. Perform final well-formedness checks and reset
581
+ * the parser tstate.
582
+ *
583
+ * @returns this
584
+ */
585
+ close() {
586
+ return this.write(null);
587
+ }
588
+ /**
589
+ * Get a single code point out of the current chunk. This updates the current
590
+ * position if we do position tracking.
591
+ *
592
+ * This is the algorithm to use for XML 1.0.
593
+ *
594
+ * @returns The character read.
595
+ */
596
+ getCode10() {
597
+ const { chunk, i } = this;
598
+ this.prevI = i;
599
+ this.i = i + 1;
600
+ if (i >= chunk.length) {
601
+ return EOC;
602
+ }
603
+ const code = chunk.charCodeAt(i);
604
+ this.column++;
605
+ if (code < 55296) {
606
+ if (code >= SPACE || code === TAB) {
607
+ return code;
608
+ }
609
+ switch (code) {
610
+ case NL:
611
+ this.line++;
612
+ this.column = 0;
613
+ this.positionAtNewLine = this.position;
614
+ return NL;
615
+ case CR:
616
+ if (chunk.charCodeAt(i + 1) === NL) {
617
+ this.i = i + 2;
618
+ }
619
+ this.line++;
620
+ this.column = 0;
621
+ this.positionAtNewLine = this.position;
622
+ return NL_LIKE;
623
+ default:
624
+ this.fail("disallowed character.");
625
+ return code;
626
+ }
627
+ }
628
+ if (code > 56319) {
629
+ if (!(code >= 57344 && code <= 65533)) {
630
+ this.fail("disallowed character.");
631
+ }
632
+ return code;
633
+ }
634
+ const final = 65536 + (code - 55296) * 1024 + (chunk.charCodeAt(i + 1) - 56320);
635
+ this.i = i + 2;
636
+ if (final > 1114111) {
637
+ this.fail("disallowed character.");
638
+ }
639
+ return final;
640
+ }
641
+ /**
642
+ * Get a single code point out of the current chunk. This updates the current
643
+ * position if we do position tracking.
644
+ *
645
+ * This is the algorithm to use for XML 1.1.
646
+ *
647
+ * @returns {number} The character read.
648
+ */
649
+ getCode11() {
650
+ const { chunk, i } = this;
651
+ this.prevI = i;
652
+ this.i = i + 1;
653
+ if (i >= chunk.length) {
654
+ return EOC;
655
+ }
656
+ const code = chunk.charCodeAt(i);
657
+ this.column++;
658
+ if (code < 55296) {
659
+ if (code > 31 && code < 127 || code > 159 && code !== LS || code === TAB) {
660
+ return code;
661
+ }
662
+ switch (code) {
663
+ case NL:
664
+ this.line++;
665
+ this.column = 0;
666
+ this.positionAtNewLine = this.position;
667
+ return NL;
668
+ case CR: {
669
+ const next = chunk.charCodeAt(i + 1);
670
+ if (next === NL || next === NEL) {
671
+ this.i = i + 2;
672
+ }
673
+ }
674
+ /* yes, fall through */
675
+ case NEL:
676
+ // 0x85
677
+ case LS:
678
+ this.line++;
679
+ this.column = 0;
680
+ this.positionAtNewLine = this.position;
681
+ return NL_LIKE;
682
+ default:
683
+ this.fail("disallowed character.");
684
+ return code;
685
+ }
686
+ }
687
+ if (code > 56319) {
688
+ if (!(code >= 57344 && code <= 65533)) {
689
+ this.fail("disallowed character.");
690
+ }
691
+ return code;
692
+ }
693
+ const final = 65536 + (code - 55296) * 1024 + (chunk.charCodeAt(i + 1) - 56320);
694
+ this.i = i + 2;
695
+ if (final > 1114111) {
696
+ this.fail("disallowed character.");
697
+ }
698
+ return final;
699
+ }
700
+ /**
701
+ * Like ``getCode`` but with the return value normalized so that ``NL`` is
702
+ * returned for ``NL_LIKE``.
703
+ */
704
+ getCodeNorm() {
705
+ const c = this.getCode();
706
+ return c === NL_LIKE ? NL : c;
707
+ }
708
+ unget() {
709
+ this.i = this.prevI;
710
+ this.column--;
711
+ }
712
+ /**
713
+ * Capture characters into a buffer until encountering one of a set of
714
+ * characters.
715
+ *
716
+ * @param chars An array of codepoints. Encountering a character in the array
717
+ * ends the capture. (``chars`` may safely contain ``NL``.)
718
+ *
719
+ * @return The character code that made the capture end, or ``EOC`` if we hit
720
+ * the end of the chunk. The return value cannot be NL_LIKE: NL is returned
721
+ * instead.
722
+ */
723
+ captureTo(chars) {
724
+ let { i: start } = this;
725
+ const { chunk } = this;
726
+ while (true) {
727
+ const c = this.getCode();
728
+ const isNLLike = c === NL_LIKE;
729
+ const final = isNLLike ? NL : c;
730
+ if (final === EOC || chars.includes(final)) {
731
+ this.text += chunk.slice(start, this.prevI);
732
+ return final;
733
+ }
734
+ if (isNLLike) {
735
+ this.text += `${chunk.slice(start, this.prevI)}
736
+ `;
737
+ start = this.i;
738
+ }
739
+ }
740
+ }
741
+ /**
742
+ * Capture characters into a buffer until encountering a character.
743
+ *
744
+ * @param char The codepoint that ends the capture. **NOTE ``char`` MAY NOT
745
+ * CONTAIN ``NL``.** Passing ``NL`` will result in buggy behavior.
746
+ *
747
+ * @return ``true`` if we ran into the character. Otherwise, we ran into the
748
+ * end of the current chunk.
749
+ */
750
+ captureToChar(char) {
751
+ let { i: start } = this;
752
+ const { chunk } = this;
753
+ while (true) {
754
+ let c = this.getCode();
755
+ switch (c) {
756
+ case NL_LIKE:
757
+ this.text += `${chunk.slice(start, this.prevI)}
758
+ `;
759
+ start = this.i;
760
+ c = NL;
761
+ break;
762
+ case EOC:
763
+ this.text += chunk.slice(start);
764
+ return false;
765
+ default:
766
+ }
767
+ if (c === char) {
768
+ this.text += chunk.slice(start, this.prevI);
769
+ return true;
770
+ }
771
+ }
772
+ }
773
+ /**
774
+ * Capture characters that satisfy ``isNameChar`` into the ``name`` field of
775
+ * this parser.
776
+ *
777
+ * @return The character code that made the test fail, or ``EOC`` if we hit
778
+ * the end of the chunk. The return value cannot be NL_LIKE: NL is returned
779
+ * instead.
780
+ */
781
+ captureNameChars() {
782
+ const { chunk, i: start } = this;
783
+ while (true) {
784
+ const c = this.getCode();
785
+ if (c === EOC) {
786
+ this.name += chunk.slice(start);
787
+ return EOC;
788
+ }
789
+ if (!isNameChar(c)) {
790
+ this.name += chunk.slice(start, this.prevI);
791
+ return c === NL_LIKE ? NL : c;
792
+ }
793
+ }
794
+ }
795
+ /**
796
+ * Skip white spaces.
797
+ *
798
+ * @return The character that ended the skip, or ``EOC`` if we hit
799
+ * the end of the chunk. The return value cannot be NL_LIKE: NL is returned
800
+ * instead.
801
+ */
802
+ skipSpaces() {
803
+ while (true) {
804
+ const c = this.getCodeNorm();
805
+ if (c === EOC || !isS(c)) {
806
+ return c;
807
+ }
808
+ }
809
+ }
810
+ setXMLVersion(version) {
811
+ this.currentXMLVersion = version;
812
+ if (version === "1.0") {
813
+ this.isChar = isChar10;
814
+ this.getCode = this.getCode10;
815
+ } else {
816
+ this.isChar = isChar11;
817
+ this.getCode = this.getCode11;
818
+ }
819
+ }
820
+ // STATE ENGINE METHODS
821
+ // This needs to be a state separate from S_BEGIN_WHITESPACE because we want
822
+ // to be sure never to come back to this state later.
823
+ sBegin() {
824
+ if (this.chunk.charCodeAt(0) === 65279) {
825
+ this.i++;
826
+ this.column++;
827
+ }
828
+ this.state = S_BEGIN_WHITESPACE;
829
+ }
830
+ sBeginWhitespace() {
831
+ const iBefore = this.i;
832
+ const c = this.skipSpaces();
833
+ if (this.prevI !== iBefore) {
834
+ this.xmlDeclPossible = false;
835
+ }
836
+ switch (c) {
837
+ case LESS:
838
+ this.state = S_OPEN_WAKA;
839
+ if (this.text.length !== 0) {
840
+ throw new Error("no-empty text at start");
841
+ }
842
+ break;
843
+ case EOC:
844
+ break;
845
+ default:
846
+ this.unget();
847
+ this.state = S_TEXT;
848
+ this.xmlDeclPossible = false;
849
+ }
850
+ }
851
+ sDoctype() {
852
+ var _a;
853
+ const c = this.captureTo(DOCTYPE_TERMINATOR);
854
+ switch (c) {
855
+ case GREATER: {
856
+ (_a = this.doctypeHandler) === null || _a === void 0 ? void 0 : _a.call(this, this.text);
857
+ this.text = "";
858
+ this.state = S_TEXT;
859
+ this.doctype = true;
860
+ break;
861
+ }
862
+ case EOC:
863
+ break;
864
+ default:
865
+ this.text += String.fromCodePoint(c);
866
+ if (c === OPEN_BRACKET) {
867
+ this.state = S_DTD;
868
+ } else if (isQuote(c)) {
869
+ this.state = S_DOCTYPE_QUOTE;
870
+ this.q = c;
871
+ }
872
+ }
873
+ }
874
+ sDoctypeQuote() {
875
+ const q = this.q;
876
+ if (this.captureToChar(q)) {
877
+ this.text += String.fromCodePoint(q);
878
+ this.q = null;
879
+ this.state = S_DOCTYPE;
880
+ }
881
+ }
882
+ sDTD() {
883
+ const c = this.captureTo(DTD_TERMINATOR);
884
+ if (c === EOC) {
885
+ return;
886
+ }
887
+ this.text += String.fromCodePoint(c);
888
+ if (c === CLOSE_BRACKET) {
889
+ this.state = S_DOCTYPE;
890
+ } else if (c === LESS) {
891
+ this.state = S_DTD_OPEN_WAKA;
892
+ } else if (isQuote(c)) {
893
+ this.state = S_DTD_QUOTED;
894
+ this.q = c;
895
+ }
896
+ }
897
+ sDTDQuoted() {
898
+ const q = this.q;
899
+ if (this.captureToChar(q)) {
900
+ this.text += String.fromCodePoint(q);
901
+ this.state = S_DTD;
902
+ this.q = null;
903
+ }
904
+ }
905
+ sDTDOpenWaka() {
906
+ const c = this.getCodeNorm();
907
+ this.text += String.fromCodePoint(c);
908
+ switch (c) {
909
+ case BANG:
910
+ this.state = S_DTD_OPEN_WAKA_BANG;
911
+ this.openWakaBang = "";
912
+ break;
913
+ case QUESTION:
914
+ this.state = S_DTD_PI;
915
+ break;
916
+ default:
917
+ this.state = S_DTD;
918
+ }
919
+ }
920
+ sDTDOpenWakaBang() {
921
+ const char = String.fromCodePoint(this.getCodeNorm());
922
+ const owb = this.openWakaBang += char;
923
+ this.text += char;
924
+ if (owb !== "-") {
925
+ this.state = owb === "--" ? S_DTD_COMMENT : S_DTD;
926
+ this.openWakaBang = "";
927
+ }
928
+ }
929
+ sDTDComment() {
930
+ if (this.captureToChar(MINUS)) {
931
+ this.text += "-";
932
+ this.state = S_DTD_COMMENT_ENDING;
933
+ }
934
+ }
935
+ sDTDCommentEnding() {
936
+ const c = this.getCodeNorm();
937
+ this.text += String.fromCodePoint(c);
938
+ this.state = c === MINUS ? S_DTD_COMMENT_ENDED : S_DTD_COMMENT;
939
+ }
940
+ sDTDCommentEnded() {
941
+ const c = this.getCodeNorm();
942
+ this.text += String.fromCodePoint(c);
943
+ if (c === GREATER) {
944
+ this.state = S_DTD;
945
+ } else {
946
+ this.fail("malformed comment.");
947
+ this.state = S_DTD_COMMENT;
948
+ }
949
+ }
950
+ sDTDPI() {
951
+ if (this.captureToChar(QUESTION)) {
952
+ this.text += "?";
953
+ this.state = S_DTD_PI_ENDING;
954
+ }
955
+ }
956
+ sDTDPIEnding() {
957
+ const c = this.getCodeNorm();
958
+ this.text += String.fromCodePoint(c);
959
+ if (c === GREATER) {
960
+ this.state = S_DTD;
961
+ }
962
+ }
963
+ sText() {
964
+ if (this.tags.length !== 0) {
965
+ this.handleTextInRoot();
966
+ } else {
967
+ this.handleTextOutsideRoot();
968
+ }
969
+ }
970
+ sEntity() {
971
+ let { i: start } = this;
972
+ const { chunk } = this;
973
+ loop:
974
+ while (true) {
975
+ switch (this.getCode()) {
976
+ case NL_LIKE:
977
+ this.entity += `${chunk.slice(start, this.prevI)}
978
+ `;
979
+ start = this.i;
980
+ break;
981
+ case SEMICOLON: {
982
+ const { entityReturnState } = this;
983
+ const entity = this.entity + chunk.slice(start, this.prevI);
984
+ this.state = entityReturnState;
985
+ let parsed;
986
+ if (entity === "") {
987
+ this.fail("empty entity name.");
988
+ parsed = "&;";
989
+ } else {
990
+ parsed = this.parseEntity(entity);
991
+ this.entity = "";
992
+ }
993
+ if (entityReturnState !== S_TEXT || this.textHandler !== void 0) {
994
+ this.text += parsed;
995
+ }
996
+ break loop;
997
+ }
998
+ case EOC:
999
+ this.entity += chunk.slice(start);
1000
+ break loop;
1001
+ default:
1002
+ }
1003
+ }
1004
+ }
1005
+ sOpenWaka() {
1006
+ const c = this.getCode();
1007
+ if (isNameStartChar(c)) {
1008
+ this.state = S_OPEN_TAG;
1009
+ this.unget();
1010
+ this.xmlDeclPossible = false;
1011
+ } else {
1012
+ switch (c) {
1013
+ case FORWARD_SLASH:
1014
+ this.state = S_CLOSE_TAG;
1015
+ this.xmlDeclPossible = false;
1016
+ break;
1017
+ case BANG:
1018
+ this.state = S_OPEN_WAKA_BANG;
1019
+ this.openWakaBang = "";
1020
+ this.xmlDeclPossible = false;
1021
+ break;
1022
+ case QUESTION:
1023
+ this.state = S_PI_FIRST_CHAR;
1024
+ break;
1025
+ default:
1026
+ this.fail("disallowed character in tag name");
1027
+ this.state = S_TEXT;
1028
+ this.xmlDeclPossible = false;
1029
+ }
1030
+ }
1031
+ }
1032
+ sOpenWakaBang() {
1033
+ this.openWakaBang += String.fromCodePoint(this.getCodeNorm());
1034
+ switch (this.openWakaBang) {
1035
+ case "[CDATA[":
1036
+ if (!this.sawRoot && !this.reportedTextBeforeRoot) {
1037
+ this.fail("text data outside of root node.");
1038
+ this.reportedTextBeforeRoot = true;
1039
+ }
1040
+ if (this.closedRoot && !this.reportedTextAfterRoot) {
1041
+ this.fail("text data outside of root node.");
1042
+ this.reportedTextAfterRoot = true;
1043
+ }
1044
+ this.state = S_CDATA;
1045
+ this.openWakaBang = "";
1046
+ break;
1047
+ case "--":
1048
+ this.state = S_COMMENT;
1049
+ this.openWakaBang = "";
1050
+ break;
1051
+ case "DOCTYPE":
1052
+ this.state = S_DOCTYPE;
1053
+ if (this.doctype || this.sawRoot) {
1054
+ this.fail("inappropriately located doctype declaration.");
1055
+ }
1056
+ this.openWakaBang = "";
1057
+ break;
1058
+ default:
1059
+ if (this.openWakaBang.length >= 7) {
1060
+ this.fail("incorrect syntax.");
1061
+ }
1062
+ }
1063
+ }
1064
+ sComment() {
1065
+ if (this.captureToChar(MINUS)) {
1066
+ this.state = S_COMMENT_ENDING;
1067
+ }
1068
+ }
1069
+ sCommentEnding() {
1070
+ var _a;
1071
+ const c = this.getCodeNorm();
1072
+ if (c === MINUS) {
1073
+ this.state = S_COMMENT_ENDED;
1074
+ (_a = this.commentHandler) === null || _a === void 0 ? void 0 : _a.call(this, this.text);
1075
+ this.text = "";
1076
+ } else {
1077
+ this.text += `-${String.fromCodePoint(c)}`;
1078
+ this.state = S_COMMENT;
1079
+ }
1080
+ }
1081
+ sCommentEnded() {
1082
+ const c = this.getCodeNorm();
1083
+ if (c !== GREATER) {
1084
+ this.fail("malformed comment.");
1085
+ this.text += `--${String.fromCodePoint(c)}`;
1086
+ this.state = S_COMMENT;
1087
+ } else {
1088
+ this.state = S_TEXT;
1089
+ }
1090
+ }
1091
+ sCData() {
1092
+ if (this.captureToChar(CLOSE_BRACKET)) {
1093
+ this.state = S_CDATA_ENDING;
1094
+ }
1095
+ }
1096
+ sCDataEnding() {
1097
+ const c = this.getCodeNorm();
1098
+ if (c === CLOSE_BRACKET) {
1099
+ this.state = S_CDATA_ENDING_2;
1100
+ } else {
1101
+ this.text += `]${String.fromCodePoint(c)}`;
1102
+ this.state = S_CDATA;
1103
+ }
1104
+ }
1105
+ sCDataEnding2() {
1106
+ var _a;
1107
+ const c = this.getCodeNorm();
1108
+ switch (c) {
1109
+ case GREATER: {
1110
+ (_a = this.cdataHandler) === null || _a === void 0 ? void 0 : _a.call(this, this.text);
1111
+ this.text = "";
1112
+ this.state = S_TEXT;
1113
+ break;
1114
+ }
1115
+ case CLOSE_BRACKET:
1116
+ this.text += "]";
1117
+ break;
1118
+ default:
1119
+ this.text += `]]${String.fromCodePoint(c)}`;
1120
+ this.state = S_CDATA;
1121
+ }
1122
+ }
1123
+ // We need this separate state to check the first character fo the pi target
1124
+ // with this.nameStartCheck which allows less characters than this.nameCheck.
1125
+ sPIFirstChar() {
1126
+ const c = this.getCodeNorm();
1127
+ if (this.nameStartCheck(c)) {
1128
+ this.piTarget += String.fromCodePoint(c);
1129
+ this.state = S_PI_REST;
1130
+ } else if (c === QUESTION || isS(c)) {
1131
+ this.fail("processing instruction without a target.");
1132
+ this.state = c === QUESTION ? S_PI_ENDING : S_PI_BODY;
1133
+ } else {
1134
+ this.fail("disallowed character in processing instruction name.");
1135
+ this.piTarget += String.fromCodePoint(c);
1136
+ this.state = S_PI_REST;
1137
+ }
1138
+ }
1139
+ sPIRest() {
1140
+ const { chunk, i: start } = this;
1141
+ while (true) {
1142
+ const c = this.getCodeNorm();
1143
+ if (c === EOC) {
1144
+ this.piTarget += chunk.slice(start);
1145
+ return;
1146
+ }
1147
+ if (!this.nameCheck(c)) {
1148
+ this.piTarget += chunk.slice(start, this.prevI);
1149
+ const isQuestion = c === QUESTION;
1150
+ if (isQuestion || isS(c)) {
1151
+ if (this.piTarget === "xml") {
1152
+ if (!this.xmlDeclPossible) {
1153
+ this.fail("an XML declaration must be at the start of the document.");
1154
+ }
1155
+ this.state = isQuestion ? S_XML_DECL_ENDING : S_XML_DECL_NAME_START;
1156
+ } else {
1157
+ this.state = isQuestion ? S_PI_ENDING : S_PI_BODY;
1158
+ }
1159
+ } else {
1160
+ this.fail("disallowed character in processing instruction name.");
1161
+ this.piTarget += String.fromCodePoint(c);
1162
+ }
1163
+ break;
1164
+ }
1165
+ }
1166
+ }
1167
+ sPIBody() {
1168
+ if (this.text.length === 0) {
1169
+ const c = this.getCodeNorm();
1170
+ if (c === QUESTION) {
1171
+ this.state = S_PI_ENDING;
1172
+ } else if (!isS(c)) {
1173
+ this.text = String.fromCodePoint(c);
1174
+ }
1175
+ } else if (this.captureToChar(QUESTION)) {
1176
+ this.state = S_PI_ENDING;
1177
+ }
1178
+ }
1179
+ sPIEnding() {
1180
+ var _a;
1181
+ const c = this.getCodeNorm();
1182
+ if (c === GREATER) {
1183
+ const { piTarget } = this;
1184
+ if (piTarget.toLowerCase() === "xml") {
1185
+ this.fail("the XML declaration must appear at the start of the document.");
1186
+ }
1187
+ (_a = this.piHandler) === null || _a === void 0 ? void 0 : _a.call(this, {
1188
+ target: piTarget,
1189
+ body: this.text
1190
+ });
1191
+ this.piTarget = this.text = "";
1192
+ this.state = S_TEXT;
1193
+ } else if (c === QUESTION) {
1194
+ this.text += "?";
1195
+ } else {
1196
+ this.text += `?${String.fromCodePoint(c)}`;
1197
+ this.state = S_PI_BODY;
1198
+ }
1199
+ this.xmlDeclPossible = false;
1200
+ }
1201
+ sXMLDeclNameStart() {
1202
+ const c = this.skipSpaces();
1203
+ if (c === QUESTION) {
1204
+ this.state = S_XML_DECL_ENDING;
1205
+ return;
1206
+ }
1207
+ if (c !== EOC) {
1208
+ this.state = S_XML_DECL_NAME;
1209
+ this.name = String.fromCodePoint(c);
1210
+ }
1211
+ }
1212
+ sXMLDeclName() {
1213
+ const c = this.captureTo(XML_DECL_NAME_TERMINATOR);
1214
+ if (c === QUESTION) {
1215
+ this.state = S_XML_DECL_ENDING;
1216
+ this.name += this.text;
1217
+ this.text = "";
1218
+ this.fail("XML declaration is incomplete.");
1219
+ return;
1220
+ }
1221
+ if (!(isS(c) || c === EQUAL)) {
1222
+ return;
1223
+ }
1224
+ this.name += this.text;
1225
+ this.text = "";
1226
+ if (!this.xmlDeclExpects.includes(this.name)) {
1227
+ switch (this.name.length) {
1228
+ case 0:
1229
+ this.fail("did not expect any more name/value pairs.");
1230
+ break;
1231
+ case 1:
1232
+ this.fail(`expected the name ${this.xmlDeclExpects[0]}.`);
1233
+ break;
1234
+ default:
1235
+ this.fail(`expected one of ${this.xmlDeclExpects.join(", ")}`);
1236
+ }
1237
+ }
1238
+ this.state = c === EQUAL ? S_XML_DECL_VALUE_START : S_XML_DECL_EQ;
1239
+ }
1240
+ sXMLDeclEq() {
1241
+ const c = this.getCodeNorm();
1242
+ if (c === QUESTION) {
1243
+ this.state = S_XML_DECL_ENDING;
1244
+ this.fail("XML declaration is incomplete.");
1245
+ return;
1246
+ }
1247
+ if (isS(c)) {
1248
+ return;
1249
+ }
1250
+ if (c !== EQUAL) {
1251
+ this.fail("value required.");
1252
+ }
1253
+ this.state = S_XML_DECL_VALUE_START;
1254
+ }
1255
+ sXMLDeclValueStart() {
1256
+ const c = this.getCodeNorm();
1257
+ if (c === QUESTION) {
1258
+ this.state = S_XML_DECL_ENDING;
1259
+ this.fail("XML declaration is incomplete.");
1260
+ return;
1261
+ }
1262
+ if (isS(c)) {
1263
+ return;
1264
+ }
1265
+ if (!isQuote(c)) {
1266
+ this.fail("value must be quoted.");
1267
+ this.q = SPACE;
1268
+ } else {
1269
+ this.q = c;
1270
+ }
1271
+ this.state = S_XML_DECL_VALUE;
1272
+ }
1273
+ sXMLDeclValue() {
1274
+ const c = this.captureTo([this.q, QUESTION]);
1275
+ if (c === QUESTION) {
1276
+ this.state = S_XML_DECL_ENDING;
1277
+ this.text = "";
1278
+ this.fail("XML declaration is incomplete.");
1279
+ return;
1280
+ }
1281
+ if (c === EOC) {
1282
+ return;
1283
+ }
1284
+ const value = this.text;
1285
+ this.text = "";
1286
+ switch (this.name) {
1287
+ case "version": {
1288
+ this.xmlDeclExpects = ["encoding", "standalone"];
1289
+ const version = value;
1290
+ this.xmlDecl.version = version;
1291
+ if (!/^1\.[0-9]+$/.test(version)) {
1292
+ this.fail("version number must match /^1\\.[0-9]+$/.");
1293
+ } else if (!this.opt.forceXMLVersion) {
1294
+ this.setXMLVersion(version);
1295
+ }
1296
+ break;
1297
+ }
1298
+ case "encoding":
1299
+ if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(value)) {
1300
+ this.fail("encoding value must match /^[A-Za-z0-9][A-Za-z0-9._-]*$/.");
1301
+ }
1302
+ this.xmlDeclExpects = ["standalone"];
1303
+ this.xmlDecl.encoding = value;
1304
+ break;
1305
+ case "standalone":
1306
+ if (value !== "yes" && value !== "no") {
1307
+ this.fail('standalone value must match "yes" or "no".');
1308
+ }
1309
+ this.xmlDeclExpects = [];
1310
+ this.xmlDecl.standalone = value;
1311
+ break;
1312
+ default:
1313
+ }
1314
+ this.name = "";
1315
+ this.state = S_XML_DECL_SEPARATOR;
1316
+ }
1317
+ sXMLDeclSeparator() {
1318
+ const c = this.getCodeNorm();
1319
+ if (c === QUESTION) {
1320
+ this.state = S_XML_DECL_ENDING;
1321
+ return;
1322
+ }
1323
+ if (!isS(c)) {
1324
+ this.fail("whitespace required.");
1325
+ this.unget();
1326
+ }
1327
+ this.state = S_XML_DECL_NAME_START;
1328
+ }
1329
+ sXMLDeclEnding() {
1330
+ var _a;
1331
+ const c = this.getCodeNorm();
1332
+ if (c === GREATER) {
1333
+ if (this.piTarget !== "xml") {
1334
+ this.fail("processing instructions are not allowed before root.");
1335
+ } else if (this.name !== "version" && this.xmlDeclExpects.includes("version")) {
1336
+ this.fail("XML declaration must contain a version.");
1337
+ }
1338
+ (_a = this.xmldeclHandler) === null || _a === void 0 ? void 0 : _a.call(this, this.xmlDecl);
1339
+ this.name = "";
1340
+ this.piTarget = this.text = "";
1341
+ this.state = S_TEXT;
1342
+ } else {
1343
+ this.fail("The character ? is disallowed anywhere in XML declarations.");
1344
+ }
1345
+ this.xmlDeclPossible = false;
1346
+ }
1347
+ sOpenTag() {
1348
+ var _a;
1349
+ const c = this.captureNameChars();
1350
+ if (c === EOC) {
1351
+ return;
1352
+ }
1353
+ const tag = this.tag = {
1354
+ name: this.name,
1355
+ attributes: /* @__PURE__ */ Object.create(null)
1356
+ };
1357
+ this.name = "";
1358
+ if (this.xmlnsOpt) {
1359
+ this.topNS = tag.ns = /* @__PURE__ */ Object.create(null);
1360
+ }
1361
+ (_a = this.openTagStartHandler) === null || _a === void 0 ? void 0 : _a.call(this, tag);
1362
+ this.sawRoot = true;
1363
+ if (!this.fragmentOpt && this.closedRoot) {
1364
+ this.fail("documents may contain only one root.");
1365
+ }
1366
+ switch (c) {
1367
+ case GREATER:
1368
+ this.openTag();
1369
+ break;
1370
+ case FORWARD_SLASH:
1371
+ this.state = S_OPEN_TAG_SLASH;
1372
+ break;
1373
+ default:
1374
+ if (!isS(c)) {
1375
+ this.fail("disallowed character in tag name.");
1376
+ }
1377
+ this.state = S_ATTRIB;
1378
+ }
1379
+ }
1380
+ sOpenTagSlash() {
1381
+ if (this.getCode() === GREATER) {
1382
+ this.openSelfClosingTag();
1383
+ } else {
1384
+ this.fail("forward-slash in opening tag not followed by >.");
1385
+ this.state = S_ATTRIB;
1386
+ }
1387
+ }
1388
+ sAttrib() {
1389
+ const c = this.skipSpaces();
1390
+ if (c === EOC) {
1391
+ return;
1392
+ }
1393
+ if (isNameStartChar(c)) {
1394
+ this.unget();
1395
+ this.state = S_ATTRIB_NAME;
1396
+ } else if (c === GREATER) {
1397
+ this.openTag();
1398
+ } else if (c === FORWARD_SLASH) {
1399
+ this.state = S_OPEN_TAG_SLASH;
1400
+ } else {
1401
+ this.fail("disallowed character in attribute name.");
1402
+ }
1403
+ }
1404
+ sAttribName() {
1405
+ const c = this.captureNameChars();
1406
+ if (c === EQUAL) {
1407
+ this.state = S_ATTRIB_VALUE;
1408
+ } else if (isS(c)) {
1409
+ this.state = S_ATTRIB_NAME_SAW_WHITE;
1410
+ } else if (c === GREATER) {
1411
+ this.fail("attribute without value.");
1412
+ this.pushAttrib(this.name, this.name);
1413
+ this.name = this.text = "";
1414
+ this.openTag();
1415
+ } else if (c !== EOC) {
1416
+ this.fail("disallowed character in attribute name.");
1417
+ }
1418
+ }
1419
+ sAttribNameSawWhite() {
1420
+ const c = this.skipSpaces();
1421
+ switch (c) {
1422
+ case EOC:
1423
+ return;
1424
+ case EQUAL:
1425
+ this.state = S_ATTRIB_VALUE;
1426
+ break;
1427
+ default:
1428
+ this.fail("attribute without value.");
1429
+ this.text = "";
1430
+ this.name = "";
1431
+ if (c === GREATER) {
1432
+ this.openTag();
1433
+ } else if (isNameStartChar(c)) {
1434
+ this.unget();
1435
+ this.state = S_ATTRIB_NAME;
1436
+ } else {
1437
+ this.fail("disallowed character in attribute name.");
1438
+ this.state = S_ATTRIB;
1439
+ }
1440
+ }
1441
+ }
1442
+ sAttribValue() {
1443
+ const c = this.getCodeNorm();
1444
+ if (isQuote(c)) {
1445
+ this.q = c;
1446
+ this.state = S_ATTRIB_VALUE_QUOTED;
1447
+ } else if (!isS(c)) {
1448
+ this.fail("unquoted attribute value.");
1449
+ this.state = S_ATTRIB_VALUE_UNQUOTED;
1450
+ this.unget();
1451
+ }
1452
+ }
1453
+ sAttribValueQuoted() {
1454
+ const { q, chunk } = this;
1455
+ let { i: start } = this;
1456
+ while (true) {
1457
+ switch (this.getCode()) {
1458
+ case q:
1459
+ this.pushAttrib(this.name, this.text + chunk.slice(start, this.prevI));
1460
+ this.name = this.text = "";
1461
+ this.q = null;
1462
+ this.state = S_ATTRIB_VALUE_CLOSED;
1463
+ return;
1464
+ case AMP:
1465
+ this.text += chunk.slice(start, this.prevI);
1466
+ this.state = S_ENTITY;
1467
+ this.entityReturnState = S_ATTRIB_VALUE_QUOTED;
1468
+ return;
1469
+ case NL:
1470
+ case NL_LIKE:
1471
+ case TAB:
1472
+ this.text += `${chunk.slice(start, this.prevI)} `;
1473
+ start = this.i;
1474
+ break;
1475
+ case LESS:
1476
+ this.text += chunk.slice(start, this.prevI);
1477
+ this.fail("disallowed character.");
1478
+ return;
1479
+ case EOC:
1480
+ this.text += chunk.slice(start);
1481
+ return;
1482
+ default:
1483
+ }
1484
+ }
1485
+ }
1486
+ sAttribValueClosed() {
1487
+ const c = this.getCodeNorm();
1488
+ if (isS(c)) {
1489
+ this.state = S_ATTRIB;
1490
+ } else if (c === GREATER) {
1491
+ this.openTag();
1492
+ } else if (c === FORWARD_SLASH) {
1493
+ this.state = S_OPEN_TAG_SLASH;
1494
+ } else if (isNameStartChar(c)) {
1495
+ this.fail("no whitespace between attributes.");
1496
+ this.unget();
1497
+ this.state = S_ATTRIB_NAME;
1498
+ } else {
1499
+ this.fail("disallowed character in attribute name.");
1500
+ }
1501
+ }
1502
+ sAttribValueUnquoted() {
1503
+ const c = this.captureTo(ATTRIB_VALUE_UNQUOTED_TERMINATOR);
1504
+ switch (c) {
1505
+ case AMP:
1506
+ this.state = S_ENTITY;
1507
+ this.entityReturnState = S_ATTRIB_VALUE_UNQUOTED;
1508
+ break;
1509
+ case LESS:
1510
+ this.fail("disallowed character.");
1511
+ break;
1512
+ case EOC:
1513
+ break;
1514
+ default:
1515
+ if (this.text.includes("]]>")) {
1516
+ this.fail('the string "]]>" is disallowed in char data.');
1517
+ }
1518
+ this.pushAttrib(this.name, this.text);
1519
+ this.name = this.text = "";
1520
+ if (c === GREATER) {
1521
+ this.openTag();
1522
+ } else {
1523
+ this.state = S_ATTRIB;
1524
+ }
1525
+ }
1526
+ }
1527
+ sCloseTag() {
1528
+ const c = this.captureNameChars();
1529
+ if (c === GREATER) {
1530
+ this.closeTag();
1531
+ } else if (isS(c)) {
1532
+ this.state = S_CLOSE_TAG_SAW_WHITE;
1533
+ } else if (c !== EOC) {
1534
+ this.fail("disallowed character in closing tag.");
1535
+ }
1536
+ }
1537
+ sCloseTagSawWhite() {
1538
+ switch (this.skipSpaces()) {
1539
+ case GREATER:
1540
+ this.closeTag();
1541
+ break;
1542
+ case EOC:
1543
+ break;
1544
+ default:
1545
+ this.fail("disallowed character in closing tag.");
1546
+ }
1547
+ }
1548
+ // END OF STATE ENGINE METHODS
1549
+ handleTextInRoot() {
1550
+ let { i: start, forbiddenState } = this;
1551
+ const { chunk, textHandler: handler } = this;
1552
+ scanLoop:
1553
+ while (true) {
1554
+ switch (this.getCode()) {
1555
+ case LESS: {
1556
+ this.state = S_OPEN_WAKA;
1557
+ if (handler !== void 0) {
1558
+ const { text } = this;
1559
+ const slice = chunk.slice(start, this.prevI);
1560
+ if (text.length !== 0) {
1561
+ handler(text + slice);
1562
+ this.text = "";
1563
+ } else if (slice.length !== 0) {
1564
+ handler(slice);
1565
+ }
1566
+ }
1567
+ forbiddenState = FORBIDDEN_START;
1568
+ break scanLoop;
1569
+ }
1570
+ case AMP:
1571
+ this.state = S_ENTITY;
1572
+ this.entityReturnState = S_TEXT;
1573
+ if (handler !== void 0) {
1574
+ this.text += chunk.slice(start, this.prevI);
1575
+ }
1576
+ forbiddenState = FORBIDDEN_START;
1577
+ break scanLoop;
1578
+ case CLOSE_BRACKET:
1579
+ switch (forbiddenState) {
1580
+ case FORBIDDEN_START:
1581
+ forbiddenState = FORBIDDEN_BRACKET;
1582
+ break;
1583
+ case FORBIDDEN_BRACKET:
1584
+ forbiddenState = FORBIDDEN_BRACKET_BRACKET;
1585
+ break;
1586
+ case FORBIDDEN_BRACKET_BRACKET:
1587
+ break;
1588
+ default:
1589
+ throw new Error("impossible state");
1590
+ }
1591
+ break;
1592
+ case GREATER:
1593
+ if (forbiddenState === FORBIDDEN_BRACKET_BRACKET) {
1594
+ this.fail('the string "]]>" is disallowed in char data.');
1595
+ }
1596
+ forbiddenState = FORBIDDEN_START;
1597
+ break;
1598
+ case NL_LIKE:
1599
+ if (handler !== void 0) {
1600
+ this.text += `${chunk.slice(start, this.prevI)}
1601
+ `;
1602
+ }
1603
+ start = this.i;
1604
+ forbiddenState = FORBIDDEN_START;
1605
+ break;
1606
+ case EOC:
1607
+ if (handler !== void 0) {
1608
+ this.text += chunk.slice(start);
1609
+ }
1610
+ break scanLoop;
1611
+ default:
1612
+ forbiddenState = FORBIDDEN_START;
1613
+ }
1614
+ }
1615
+ this.forbiddenState = forbiddenState;
1616
+ }
1617
+ handleTextOutsideRoot() {
1618
+ let { i: start } = this;
1619
+ const { chunk, textHandler: handler } = this;
1620
+ let nonSpace = false;
1621
+ outRootLoop:
1622
+ while (true) {
1623
+ const code = this.getCode();
1624
+ switch (code) {
1625
+ case LESS: {
1626
+ this.state = S_OPEN_WAKA;
1627
+ if (handler !== void 0) {
1628
+ const { text } = this;
1629
+ const slice = chunk.slice(start, this.prevI);
1630
+ if (text.length !== 0) {
1631
+ handler(text + slice);
1632
+ this.text = "";
1633
+ } else if (slice.length !== 0) {
1634
+ handler(slice);
1635
+ }
1636
+ }
1637
+ break outRootLoop;
1638
+ }
1639
+ case AMP:
1640
+ this.state = S_ENTITY;
1641
+ this.entityReturnState = S_TEXT;
1642
+ if (handler !== void 0) {
1643
+ this.text += chunk.slice(start, this.prevI);
1644
+ }
1645
+ nonSpace = true;
1646
+ break outRootLoop;
1647
+ case NL_LIKE:
1648
+ if (handler !== void 0) {
1649
+ this.text += `${chunk.slice(start, this.prevI)}
1650
+ `;
1651
+ }
1652
+ start = this.i;
1653
+ break;
1654
+ case EOC:
1655
+ if (handler !== void 0) {
1656
+ this.text += chunk.slice(start);
1657
+ }
1658
+ break outRootLoop;
1659
+ default:
1660
+ if (!isS(code)) {
1661
+ nonSpace = true;
1662
+ }
1663
+ }
1664
+ }
1665
+ if (!nonSpace) {
1666
+ return;
1667
+ }
1668
+ if (!this.sawRoot && !this.reportedTextBeforeRoot) {
1669
+ this.fail("text data outside of root node.");
1670
+ this.reportedTextBeforeRoot = true;
1671
+ }
1672
+ if (this.closedRoot && !this.reportedTextAfterRoot) {
1673
+ this.fail("text data outside of root node.");
1674
+ this.reportedTextAfterRoot = true;
1675
+ }
1676
+ }
1677
+ pushAttribNS(name, value) {
1678
+ var _a;
1679
+ const { prefix, local } = this.qname(name);
1680
+ const attr = { name, prefix, local, value };
1681
+ this.attribList.push(attr);
1682
+ (_a = this.attributeHandler) === null || _a === void 0 ? void 0 : _a.call(this, attr);
1683
+ if (prefix === "xmlns") {
1684
+ const trimmed = value.trim();
1685
+ if (this.currentXMLVersion === "1.0" && trimmed === "") {
1686
+ this.fail("invalid attempt to undefine prefix in XML 1.0");
1687
+ }
1688
+ this.topNS[local] = trimmed;
1689
+ nsPairCheck(this, local, trimmed);
1690
+ } else if (name === "xmlns") {
1691
+ const trimmed = value.trim();
1692
+ this.topNS[""] = trimmed;
1693
+ nsPairCheck(this, "", trimmed);
1694
+ }
1695
+ }
1696
+ pushAttribPlain(name, value) {
1697
+ var _a;
1698
+ const attr = { name, value };
1699
+ this.attribList.push(attr);
1700
+ (_a = this.attributeHandler) === null || _a === void 0 ? void 0 : _a.call(this, attr);
1701
+ }
1702
+ /**
1703
+ * End parsing. This performs final well-formedness checks and resets the
1704
+ * parser to a clean state.
1705
+ *
1706
+ * @returns this
1707
+ */
1708
+ end() {
1709
+ var _a, _b;
1710
+ if (!this.sawRoot) {
1711
+ this.fail("document must contain a root element.");
1712
+ }
1713
+ const { tags } = this;
1714
+ while (tags.length > 0) {
1715
+ const tag = tags.pop();
1716
+ this.fail(`unclosed tag: ${tag.name}`);
1717
+ }
1718
+ if (this.state !== S_BEGIN && this.state !== S_TEXT) {
1719
+ this.fail("unexpected end.");
1720
+ }
1721
+ const { text } = this;
1722
+ if (text.length !== 0) {
1723
+ (_a = this.textHandler) === null || _a === void 0 ? void 0 : _a.call(this, text);
1724
+ this.text = "";
1725
+ }
1726
+ this._closed = true;
1727
+ (_b = this.endHandler) === null || _b === void 0 ? void 0 : _b.call(this);
1728
+ this._init();
1729
+ return this;
1730
+ }
1731
+ /**
1732
+ * Resolve a namespace prefix.
1733
+ *
1734
+ * @param prefix The prefix to resolve.
1735
+ *
1736
+ * @returns The namespace URI or ``undefined`` if the prefix is not defined.
1737
+ */
1738
+ resolve(prefix) {
1739
+ var _a, _b;
1740
+ let uri = this.topNS[prefix];
1741
+ if (uri !== void 0) {
1742
+ return uri;
1743
+ }
1744
+ const { tags } = this;
1745
+ for (let index = tags.length - 1; index >= 0; index--) {
1746
+ uri = tags[index].ns[prefix];
1747
+ if (uri !== void 0) {
1748
+ return uri;
1749
+ }
1750
+ }
1751
+ uri = this.ns[prefix];
1752
+ if (uri !== void 0) {
1753
+ return uri;
1754
+ }
1755
+ return (_b = (_a = this.opt).resolvePrefix) === null || _b === void 0 ? void 0 : _b.call(_a, prefix);
1756
+ }
1757
+ /**
1758
+ * Parse a qname into its prefix and local name parts.
1759
+ *
1760
+ * @param name The name to parse
1761
+ *
1762
+ * @returns
1763
+ */
1764
+ qname(name) {
1765
+ const colon = name.indexOf(":");
1766
+ if (colon === -1) {
1767
+ return { prefix: "", local: name };
1768
+ }
1769
+ const local = name.slice(colon + 1);
1770
+ const prefix = name.slice(0, colon);
1771
+ if (prefix === "" || local === "" || local.includes(":")) {
1772
+ this.fail(`malformed name: ${name}.`);
1773
+ }
1774
+ return { prefix, local };
1775
+ }
1776
+ processAttribsNS() {
1777
+ var _a;
1778
+ const { attribList } = this;
1779
+ const tag = this.tag;
1780
+ {
1781
+ const { prefix, local } = this.qname(tag.name);
1782
+ tag.prefix = prefix;
1783
+ tag.local = local;
1784
+ const uri = tag.uri = (_a = this.resolve(prefix)) !== null && _a !== void 0 ? _a : "";
1785
+ if (prefix !== "") {
1786
+ if (prefix === "xmlns") {
1787
+ this.fail('tags may not have "xmlns" as prefix.');
1788
+ }
1789
+ if (uri === "") {
1790
+ this.fail(`unbound namespace prefix: ${JSON.stringify(prefix)}.`);
1791
+ tag.uri = prefix;
1792
+ }
1793
+ }
1794
+ }
1795
+ if (attribList.length === 0) {
1796
+ return;
1797
+ }
1798
+ const { attributes } = tag;
1799
+ const seen = /* @__PURE__ */ new Set();
1800
+ for (const attr of attribList) {
1801
+ const { name, prefix, local } = attr;
1802
+ let uri;
1803
+ let eqname;
1804
+ if (prefix === "") {
1805
+ uri = name === "xmlns" ? XMLNS_NAMESPACE : "";
1806
+ eqname = name;
1807
+ } else {
1808
+ uri = this.resolve(prefix);
1809
+ if (uri === void 0) {
1810
+ this.fail(`unbound namespace prefix: ${JSON.stringify(prefix)}.`);
1811
+ uri = prefix;
1812
+ }
1813
+ eqname = `{${uri}}${local}`;
1814
+ }
1815
+ if (seen.has(eqname)) {
1816
+ this.fail(`duplicate attribute: ${eqname}.`);
1817
+ }
1818
+ seen.add(eqname);
1819
+ attr.uri = uri;
1820
+ attributes[name] = attr;
1821
+ }
1822
+ this.attribList = [];
1823
+ }
1824
+ processAttribsPlain() {
1825
+ const { attribList } = this;
1826
+ const attributes = this.tag.attributes;
1827
+ for (const { name, value } of attribList) {
1828
+ if (attributes[name] !== void 0) {
1829
+ this.fail(`duplicate attribute: ${name}.`);
1830
+ }
1831
+ attributes[name] = value;
1832
+ }
1833
+ this.attribList = [];
1834
+ }
1835
+ /**
1836
+ * Handle a complete open tag. This parser code calls this once it has seen
1837
+ * the whole tag. This method checks for well-formeness and then emits
1838
+ * ``onopentag``.
1839
+ */
1840
+ openTag() {
1841
+ var _a;
1842
+ this.processAttribs();
1843
+ const { tags } = this;
1844
+ const tag = this.tag;
1845
+ tag.isSelfClosing = false;
1846
+ (_a = this.openTagHandler) === null || _a === void 0 ? void 0 : _a.call(this, tag);
1847
+ tags.push(tag);
1848
+ this.state = S_TEXT;
1849
+ this.name = "";
1850
+ }
1851
+ /**
1852
+ * Handle a complete self-closing tag. This parser code calls this once it has
1853
+ * seen the whole tag. This method checks for well-formeness and then emits
1854
+ * ``onopentag`` and ``onclosetag``.
1855
+ */
1856
+ openSelfClosingTag() {
1857
+ var _a, _b, _c;
1858
+ this.processAttribs();
1859
+ const { tags } = this;
1860
+ const tag = this.tag;
1861
+ tag.isSelfClosing = true;
1862
+ (_a = this.openTagHandler) === null || _a === void 0 ? void 0 : _a.call(this, tag);
1863
+ (_b = this.closeTagHandler) === null || _b === void 0 ? void 0 : _b.call(this, tag);
1864
+ const top = this.tag = (_c = tags[tags.length - 1]) !== null && _c !== void 0 ? _c : null;
1865
+ if (top === null) {
1866
+ this.closedRoot = true;
1867
+ }
1868
+ this.state = S_TEXT;
1869
+ this.name = "";
1870
+ }
1871
+ /**
1872
+ * Handle a complete close tag. This parser code calls this once it has seen
1873
+ * the whole tag. This method checks for well-formeness and then emits
1874
+ * ``onclosetag``.
1875
+ */
1876
+ closeTag() {
1877
+ const { tags, name } = this;
1878
+ this.state = S_TEXT;
1879
+ this.name = "";
1880
+ if (name === "") {
1881
+ this.fail("weird empty close tag.");
1882
+ this.text += "</>";
1883
+ return;
1884
+ }
1885
+ const handler = this.closeTagHandler;
1886
+ let l = tags.length;
1887
+ while (l-- > 0) {
1888
+ const tag = this.tag = tags.pop();
1889
+ this.topNS = tag.ns;
1890
+ handler === null || handler === void 0 ? void 0 : handler(tag);
1891
+ if (tag.name === name) {
1892
+ break;
1893
+ }
1894
+ this.fail("unexpected close tag.");
1895
+ }
1896
+ if (l === 0) {
1897
+ this.closedRoot = true;
1898
+ } else if (l < 0) {
1899
+ this.fail(`unmatched closing tag: ${name}.`);
1900
+ this.text += `</${name}>`;
1901
+ }
1902
+ }
1903
+ /**
1904
+ * Resolves an entity. Makes any necessary well-formedness checks.
1905
+ *
1906
+ * @param entity The entity to resolve.
1907
+ *
1908
+ * @returns The parsed entity.
1909
+ */
1910
+ parseEntity(entity) {
1911
+ if (entity[0] !== "#") {
1912
+ const defined = this.ENTITIES[entity];
1913
+ if (defined !== void 0) {
1914
+ return defined;
1915
+ }
1916
+ this.fail(this.isName(entity) ? "undefined entity." : "disallowed character in entity name.");
1917
+ return `&${entity};`;
1918
+ }
1919
+ let num = NaN;
1920
+ if (entity[1] === "x" && /^#x[0-9a-f]+$/i.test(entity)) {
1921
+ num = parseInt(entity.slice(2), 16);
1922
+ } else if (/^#[0-9]+$/.test(entity)) {
1923
+ num = parseInt(entity.slice(1), 10);
1924
+ }
1925
+ if (!this.isChar(num)) {
1926
+ this.fail("malformed character entity.");
1927
+ return `&${entity};`;
1928
+ }
1929
+ return String.fromCodePoint(num);
1930
+ }
1931
+ };
1932
+ exports.SaxesParser = SaxesParser2;
1933
+ }
1934
+ });
2
1935
 
3
1936
  // ../core/dist/server.js
4
1937
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
@@ -29,7 +1962,6 @@ function toPublic(inst) {
29
1962
  pluginVersion: inst.pluginVersion,
30
1963
  pluginVariant: inst.pluginVariant,
31
1964
  serverVersion: inst.serverVersion,
32
- versionMismatch: inst.versionMismatch,
33
1965
  lastActivity: inst.lastActivity,
34
1966
  connectedAt: inst.connectedAt
35
1967
  };
@@ -37,8 +1969,10 @@ function toPublic(inst) {
37
1969
  var STALE_INSTANCE_MS = 3e4;
38
1970
  var DUPLICATE_TAKEOVER_MS = 3e3;
39
1971
  var INSTANCE_ALIAS_TTL_MS = 5 * 60 * 1e3;
40
- var DEFAULT_POLL_WAIT_TIMEOUT_MS = 15e3;
41
- var MAX_ACTIVE_POLLS = 128;
1972
+ var ACCEPTED_REQUEST_TOMBSTONE_TTL_MS = 6e4;
1973
+ var MAX_ACCEPTED_REQUEST_TOMBSTONES = 4096;
1974
+ var CANCELLATION_TOMBSTONE_TTL_MS = 6e4;
1975
+ var MAX_CANCELLATION_TOMBSTONES = 4096;
42
1976
  function publishedInstanceId(placeId) {
43
1977
  if (placeId === void 0 || !Number.isFinite(placeId) || placeId <= 0)
44
1978
  return void 0;
@@ -46,11 +1980,15 @@ function publishedInstanceId(placeId) {
46
1980
  }
47
1981
  var BridgeService = class {
48
1982
  pendingRequests = /* @__PURE__ */ new Map();
1983
+ acceptedRequestIds = /* @__PURE__ */ new Map();
1984
+ pendingCancellations = /* @__PURE__ */ new Map();
49
1985
  // Keyed by pluginSessionId (the per-plugin GUID).
50
1986
  instances = /* @__PURE__ */ new Map();
51
1987
  instanceAliases = /* @__PURE__ */ new Map();
52
1988
  instanceRegisteredListeners = /* @__PURE__ */ new Set();
53
- pendingPolls = /* @__PURE__ */ new Map();
1989
+ requestAvailableListeners = /* @__PURE__ */ new Set();
1990
+ sessionClosedListeners = /* @__PURE__ */ new Set();
1991
+ deliveryOwnersByPhysicalSession = /* @__PURE__ */ new Map();
54
1992
  requestTimeout = 3e4;
55
1993
  onInstanceRegistered(listener) {
56
1994
  this.instanceRegisteredListeners.add(listener);
@@ -70,6 +2008,62 @@ var BridgeService = class {
70
2008
  }
71
2009
  }
72
2010
  }
2011
+ onRequestAvailable(listener) {
2012
+ this.requestAvailableListeners.add(listener);
2013
+ return () => this.requestAvailableListeners.delete(listener);
2014
+ }
2015
+ onSessionClosed(listener) {
2016
+ this.sessionClosedListeners.add(listener);
2017
+ return () => this.sessionClosedListeners.delete(listener);
2018
+ }
2019
+ setDeliveryActive(physicalSessionId, owner, active) {
2020
+ let owners = this.deliveryOwnersByPhysicalSession.get(physicalSessionId);
2021
+ if (active) {
2022
+ if (!owners) {
2023
+ owners = /* @__PURE__ */ new Set();
2024
+ this.deliveryOwnersByPhysicalSession.set(physicalSessionId, owners);
2025
+ }
2026
+ owners.add(owner);
2027
+ return;
2028
+ }
2029
+ if (!owners)
2030
+ return;
2031
+ owners.delete(owner);
2032
+ if (owners.size === 0)
2033
+ this.deliveryOwnersByPhysicalSession.delete(physicalSessionId);
2034
+ }
2035
+ notifyRequestAvailable(physicalSessionId) {
2036
+ for (const listener of this.requestAvailableListeners) {
2037
+ try {
2038
+ listener(physicalSessionId);
2039
+ } catch {
2040
+ }
2041
+ }
2042
+ }
2043
+ notifyRequestCancelled(request, reason) {
2044
+ const physicalSessionId = request.lastDeliveryPhysicalSessionId;
2045
+ if (!physicalSessionId || this.pendingCancellations.has(request.id))
2046
+ return;
2047
+ const now = Date.now();
2048
+ this.prunePendingCancellations(now);
2049
+ this.pendingCancellations.set(request.id, {
2050
+ requestId: request.id,
2051
+ reason,
2052
+ physicalSessionId,
2053
+ createdAt: now
2054
+ });
2055
+ this.prunePendingCancellations(now);
2056
+ this.notifyRequestAvailable(physicalSessionId);
2057
+ }
2058
+ physicalSessionsForTarget(targetInstanceId, targetRole) {
2059
+ const physicalSessionIds = /* @__PURE__ */ new Set();
2060
+ for (const instance of this.instances.values()) {
2061
+ if (instance.instanceId === targetInstanceId && instance.role === targetRole) {
2062
+ physicalSessionIds.add(instance.physicalSessionId);
2063
+ }
2064
+ }
2065
+ return Array.from(physicalSessionIds);
2066
+ }
73
2067
  canonicalInstanceId(instanceId, placeId) {
74
2068
  return publishedInstanceId(placeId) ?? instanceId;
75
2069
  }
@@ -140,7 +2134,6 @@ var BridgeService = class {
140
2134
  const pluginVersion = input.pluginVersion ?? "";
141
2135
  const pluginVariant = input.pluginVariant ?? "unknown";
142
2136
  const serverVersion = input.serverVersion ?? "";
143
- const versionMismatch = pluginVersion !== "" && serverVersion !== "" && pluginVersion !== serverVersion;
144
2137
  this.rememberInstanceAlias(rawInstanceId, instanceId);
145
2138
  if (prior && prior.instanceId !== instanceId) {
146
2139
  this.rememberInstanceAlias(prior.instanceId, instanceId);
@@ -166,7 +2159,8 @@ var BridgeService = class {
166
2159
  }
167
2160
  const existing = Array.from(this.instances.values()).find((i) => i.instanceId === instanceId && i.role === assignedRole && i.pluginSessionId !== pluginSessionId);
168
2161
  if (existing) {
169
- if (!this.pendingPolls.has(existing.pluginSessionId) && Date.now() - existing.lastActivity > DUPLICATE_TAKEOVER_MS) {
2162
+ const existingDeliveryActive = (this.deliveryOwnersByPhysicalSession.get(existing.physicalSessionId)?.size ?? 0) > 0;
2163
+ if (!existingDeliveryActive && Date.now() - existing.lastActivity > DUPLICATE_TAKEOVER_MS) {
170
2164
  this.unregisterInstance(existing.pluginSessionId);
171
2165
  } else {
172
2166
  return {
@@ -181,6 +2175,7 @@ var BridgeService = class {
181
2175
  }
182
2176
  const registered = {
183
2177
  pluginSessionId,
2178
+ physicalSessionId: input.physicalSessionId,
184
2179
  instanceId,
185
2180
  role: assignedRole,
186
2181
  placeId: input.placeId ?? 0,
@@ -190,29 +2185,39 @@ var BridgeService = class {
190
2185
  pluginVersion,
191
2186
  pluginVariant,
192
2187
  serverVersion,
193
- versionMismatch,
194
2188
  lastActivity: Date.now(),
195
2189
  connectedAt: prior?.connectedAt ?? Date.now()
196
2190
  };
197
2191
  this.instances.set(pluginSessionId, registered);
198
2192
  this.notifyInstanceRegistered(toPublic(registered));
199
- this.dispatchPendingPolls(instanceId, assignedRole);
2193
+ this.notifyRequestAvailable(registered.physicalSessionId);
200
2194
  return { ok: true, assignedRole, instanceId };
201
2195
  }
202
2196
  unregisterInstance(pluginSessionId) {
203
2197
  const removed = this.instances.get(pluginSessionId);
204
- this.instances.delete(pluginSessionId);
205
- const pendingPoll = this.pendingPolls.get(pluginSessionId);
206
- if (pendingPoll) {
207
- this.settlePendingPoll(pendingPoll, { kind: "session_closed" });
208
- }
209
2198
  if (!removed)
210
2199
  return;
211
- for (const [id, req] of this.pendingRequests.entries()) {
2200
+ const logicalChildren = removed.physicalSessionId === pluginSessionId ? Array.from(this.instances.values()).filter((instance) => instance.pluginSessionId !== pluginSessionId && instance.physicalSessionId === pluginSessionId) : [];
2201
+ this.instances.delete(pluginSessionId);
2202
+ const session = {
2203
+ logicalSessionId: pluginSessionId,
2204
+ physicalSessionId: removed.physicalSessionId
2205
+ };
2206
+ for (const listener of this.sessionClosedListeners) {
2207
+ try {
2208
+ listener(session);
2209
+ } catch {
2210
+ }
2211
+ }
2212
+ if (removed.physicalSessionId === pluginSessionId) {
2213
+ this.deliveryOwnersByPhysicalSession.delete(pluginSessionId);
2214
+ }
2215
+ for (const child of logicalChildren)
2216
+ this.unregisterInstance(child.pluginSessionId);
2217
+ for (const req of Array.from(this.pendingRequests.values())) {
212
2218
  const stillHasHandler = Array.from(this.instances.values()).some((i) => i.instanceId === req.targetInstanceId && i.role === req.targetRole);
213
2219
  if (!stillHasHandler) {
214
- clearTimeout(req.timeoutId);
215
- this.pendingRequests.delete(id);
2220
+ this.removePendingRequest(req);
216
2221
  req.reject(new Error(`Target (${req.targetInstanceId}, ${req.targetRole}) disconnected`));
217
2222
  }
218
2223
  }
@@ -241,9 +2246,14 @@ var BridgeService = class {
241
2246
  return this.pendingRequests.size;
242
2247
  }
243
2248
  updateInstanceActivity(pluginSessionId) {
244
- const inst = this.instances.get(pluginSessionId);
245
- if (inst) {
246
- inst.lastActivity = Date.now();
2249
+ const instance = this.instances.get(pluginSessionId);
2250
+ if (!instance)
2251
+ return;
2252
+ const now = Date.now();
2253
+ for (const candidate of this.instances.values()) {
2254
+ if (candidate.physicalSessionId === instance.physicalSessionId) {
2255
+ candidate.lastActivity = now;
2256
+ }
247
2257
  }
248
2258
  }
249
2259
  updateInstanceMetadata(pluginSessionId, metadata) {
@@ -266,14 +2276,15 @@ var BridgeService = class {
266
2276
  this.rememberInstanceAlias(priorInstanceId, canonicalInstanceId);
267
2277
  this.migratePendingRequests(priorInstanceId, canonicalInstanceId);
268
2278
  inst.instanceId = canonicalInstanceId;
269
- this.dispatchPendingPolls(canonicalInstanceId, inst.role);
2279
+ this.notifyRequestAvailable(inst.physicalSessionId);
270
2280
  }
271
2281
  }
272
2282
  }
273
2283
  cleanupStaleInstances() {
274
2284
  const now = Date.now();
275
2285
  for (const [id, inst] of this.instances.entries()) {
276
- if (!this.pendingPolls.has(id) && now - inst.lastActivity > STALE_INSTANCE_MS) {
2286
+ const deliveryActive = (this.deliveryOwnersByPhysicalSession.get(inst.physicalSessionId)?.size ?? 0) > 0;
2287
+ if (!deliveryActive && now - inst.lastActivity > STALE_INSTANCE_MS) {
277
2288
  this.unregisterInstance(id);
278
2289
  }
279
2290
  }
@@ -389,23 +2400,28 @@ var BridgeService = class {
389
2400
  };
390
2401
  }
391
2402
  if (distinctInstanceIds.size > 1) {
392
- const errorCode = role ? "ambiguous_target" : "multiple_instances_connected";
2403
+ const errorCode2 = role ? "ambiguous_target" : "multiple_instances_connected";
393
2404
  const msg = role ? `target=${role} is ambiguous because multiple Studio places are connected. Pass instance_id to choose a place.` : "Multiple Studio places are connected. Pass instance_id to disambiguate.";
394
- return { ok: false, error: { code: errorCode, message: msg, data: errorData } };
2405
+ return { ok: false, error: { code: errorCode2, message: msg, data: errorData } };
395
2406
  }
396
2407
  const onlyInstanceId = distinctInstanceIds.values().next().value;
397
2408
  return this.resolveTarget({ instance_id: onlyInstanceId, target });
398
2409
  }
399
- async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.requestTimeout) {
2410
+ async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.requestTimeout, signal) {
400
2411
  const requestId = randomUUID();
401
2412
  const effectiveTimeoutMs = Math.max(1, timeoutMs);
2413
+ if (signal?.aborted)
2414
+ throw new Error("Request aborted");
402
2415
  return new Promise((resolve5, reject) => {
403
- const timeoutId = setTimeout(() => {
404
- if (this.pendingRequests.has(requestId)) {
405
- this.pendingRequests.delete(requestId);
406
- reject(new Error("Request timeout"));
407
- }
408
- }, effectiveTimeoutMs);
2416
+ const cancelPending = (reason, error) => {
2417
+ const pending = this.pendingRequests.get(requestId);
2418
+ if (!pending || !this.removePendingRequest(pending))
2419
+ return;
2420
+ this.notifyRequestCancelled(pending, reason);
2421
+ pending.reject(error);
2422
+ };
2423
+ const timeoutId = setTimeout(() => cancelPending("timeout", new Error("Request timeout")), effectiveTimeoutMs);
2424
+ const abortListener = () => cancelPending("aborted", new Error("Request aborted"));
409
2425
  const request = {
410
2426
  id: requestId,
411
2427
  endpoint,
@@ -413,159 +2429,158 @@ var BridgeService = class {
413
2429
  targetInstanceId,
414
2430
  targetRole,
415
2431
  timestamp: Date.now(),
416
- inFlight: false,
417
2432
  resolve: resolve5,
418
2433
  reject,
419
2434
  timeoutId,
420
- timeoutMs: effectiveTimeoutMs
2435
+ timeoutMs: effectiveTimeoutMs,
2436
+ abortSignal: signal,
2437
+ abortListener
421
2438
  };
422
2439
  this.pendingRequests.set(requestId, request);
423
- this.dispatchPendingPolls(targetInstanceId, targetRole);
2440
+ signal?.addEventListener("abort", abortListener, { once: true });
2441
+ if (signal?.aborted)
2442
+ abortListener();
2443
+ if (this.pendingRequests.has(requestId)) {
2444
+ for (const physicalSessionId of this.physicalSessionsForTarget(targetInstanceId, targetRole)) {
2445
+ this.notifyRequestAvailable(physicalSessionId);
2446
+ }
2447
+ }
424
2448
  });
425
2449
  }
426
- getPendingRequest(callerInstanceId, callerRole) {
427
- let oldestRequest = null;
2450
+ removePendingRequest(request) {
2451
+ if (this.pendingRequests.get(request.id) !== request)
2452
+ return false;
2453
+ clearTimeout(request.timeoutId);
2454
+ if (request.abortSignal && request.abortListener) {
2455
+ request.abortSignal.removeEventListener("abort", request.abortListener);
2456
+ }
2457
+ this.pendingRequests.delete(request.id);
2458
+ return true;
2459
+ }
2460
+ claimNextRequestForPhysical(physicalSessionId, claimOwner) {
2461
+ let oldestRequest;
2462
+ let logicalSessionId = "";
428
2463
  for (const request of this.pendingRequests.values()) {
429
- if (request.targetInstanceId !== callerInstanceId)
2464
+ if (request.claimOwner !== void 0)
430
2465
  continue;
431
- if (request.targetRole !== callerRole)
432
- continue;
433
- if (request.inFlight)
2466
+ let matchingSessionId;
2467
+ for (const candidate of this.instances.values()) {
2468
+ if (candidate.physicalSessionId === physicalSessionId && candidate.instanceId === request.targetInstanceId && candidate.role === request.targetRole) {
2469
+ matchingSessionId = candidate.pluginSessionId;
2470
+ break;
2471
+ }
2472
+ }
2473
+ if (!matchingSessionId)
434
2474
  continue;
435
2475
  if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
436
2476
  oldestRequest = request;
2477
+ logicalSessionId = matchingSessionId;
437
2478
  }
438
2479
  }
439
- if (oldestRequest) {
440
- oldestRequest.inFlight = true;
441
- return {
442
- requestId: oldestRequest.id,
443
- request: {
444
- endpoint: oldestRequest.endpoint,
445
- data: oldestRequest.data
446
- }
447
- };
448
- }
449
- return null;
450
- }
451
- waitForPendingRequest(pluginSessionId, options = {}) {
452
- const instance = this.instances.get(pluginSessionId);
453
- if (!instance) {
454
- return Promise.resolve({ kind: "unknown_session" });
455
- }
456
- if (options.signal?.aborted) {
457
- return Promise.resolve({ kind: "aborted" });
458
- }
459
- this.updateInstanceActivity(pluginSessionId);
460
- const existingPoll = this.pendingPolls.get(pluginSessionId);
461
- if (existingPoll) {
462
- this.settlePendingPoll(existingPoll, { kind: "superseded" });
463
- }
464
- const pendingRequest = this.getPendingRequest(instance.instanceId, instance.role);
465
- if (pendingRequest) {
466
- return Promise.resolve({
467
- kind: "request",
468
- requestId: pendingRequest.requestId,
469
- request: pendingRequest.request
470
- });
471
- }
472
- if (this.pendingPolls.size >= MAX_ACTIVE_POLLS) {
473
- return Promise.resolve({ kind: "capacity" });
474
- }
475
- const timeoutMs = Math.min(DEFAULT_POLL_WAIT_TIMEOUT_MS, Math.max(1, options.timeoutMs ?? DEFAULT_POLL_WAIT_TIMEOUT_MS));
476
- let resolvePoll;
477
- const promise = new Promise((resolve5) => {
478
- resolvePoll = resolve5;
479
- });
480
- let waiter;
481
- const timeoutId = setTimeout(() => {
482
- this.settlePendingPoll(waiter, { kind: "timeout" }, true);
483
- }, timeoutMs);
484
- timeoutId.unref();
485
- waiter = {
486
- pluginSessionId,
487
- timeoutId,
488
- signal: options.signal,
489
- resolve: resolvePoll
2480
+ if (!oldestRequest)
2481
+ return null;
2482
+ oldestRequest.claimOwner = claimOwner;
2483
+ oldestRequest.lastDeliveryPhysicalSessionId = physicalSessionId;
2484
+ return {
2485
+ requestId: oldestRequest.id,
2486
+ logicalSessionId,
2487
+ target: oldestRequest.targetRole,
2488
+ endpoint: oldestRequest.endpoint,
2489
+ data: oldestRequest.data,
2490
+ remainingMs: Math.max(1, oldestRequest.timeoutMs - (Date.now() - oldestRequest.timestamp))
490
2491
  };
491
- if (options.signal) {
492
- waiter.onAbort = () => {
493
- this.settlePendingPoll(waiter, { kind: "aborted" });
494
- };
495
- options.signal.addEventListener("abort", waiter.onAbort, { once: true });
496
- }
497
- this.pendingPolls.set(pluginSessionId, waiter);
498
- return promise;
499
2492
  }
500
- dispatchPendingPolls(targetInstanceId, targetRole) {
501
- for (const waiter of Array.from(this.pendingPolls.values())) {
502
- const instance = this.instances.get(waiter.pluginSessionId);
503
- if (!instance) {
504
- this.settlePendingPoll(waiter, { kind: "session_closed" });
2493
+ claimNextCancellationForPhysical(physicalSessionId, claimOwner) {
2494
+ this.prunePendingCancellations(Date.now());
2495
+ for (const cancellation of this.pendingCancellations.values()) {
2496
+ if (cancellation.physicalSessionId !== physicalSessionId || cancellation.claimOwner !== void 0) {
505
2497
  continue;
506
2498
  }
507
- if (targetInstanceId !== void 0 && instance.instanceId !== targetInstanceId)
508
- continue;
509
- if (targetRole !== void 0 && instance.role !== targetRole)
510
- continue;
511
- const pendingRequest = this.getPendingRequest(instance.instanceId, instance.role);
512
- if (!pendingRequest)
513
- continue;
514
- this.settlePendingPoll(waiter, {
515
- kind: "request",
516
- requestId: pendingRequest.requestId,
517
- request: pendingRequest.request
518
- }, true);
2499
+ cancellation.claimOwner = claimOwner;
2500
+ return { requestId: cancellation.requestId, reason: cancellation.reason };
519
2501
  }
2502
+ return null;
520
2503
  }
521
- settlePendingPoll(waiter, result, refreshActivity = false) {
522
- if (this.pendingPolls.get(waiter.pluginSessionId) !== waiter)
523
- return;
524
- this.pendingPolls.delete(waiter.pluginSessionId);
525
- clearTimeout(waiter.timeoutId);
526
- if (waiter.signal && waiter.onAbort) {
527
- waiter.signal.removeEventListener("abort", waiter.onAbort);
2504
+ releaseDeliveryClaims(claimOwner) {
2505
+ const physicalSessionIds = /* @__PURE__ */ new Set();
2506
+ for (const request of this.pendingRequests.values()) {
2507
+ if (request.claimOwner !== claimOwner)
2508
+ continue;
2509
+ request.claimOwner = void 0;
2510
+ for (const physicalSessionId of this.physicalSessionsForTarget(request.targetInstanceId, request.targetRole)) {
2511
+ physicalSessionIds.add(physicalSessionId);
2512
+ }
2513
+ }
2514
+ for (const cancellation of this.pendingCancellations.values()) {
2515
+ if (cancellation.claimOwner !== claimOwner)
2516
+ continue;
2517
+ cancellation.claimOwner = void 0;
2518
+ physicalSessionIds.add(cancellation.physicalSessionId);
528
2519
  }
529
- if (refreshActivity) {
530
- this.updateInstanceActivity(waiter.pluginSessionId);
2520
+ for (const physicalSessionId of physicalSessionIds) {
2521
+ this.notifyRequestAvailable(physicalSessionId);
531
2522
  }
532
- waiter.resolve(result);
533
2523
  }
534
2524
  resolveRequest(requestId, response) {
535
- const request = this.pendingRequests.get(requestId);
536
- if (request) {
537
- clearTimeout(request.timeoutId);
538
- this.pendingRequests.delete(requestId);
539
- request.resolve(response);
540
- }
2525
+ return this.settleRequest(requestId, (request) => request.resolve(response));
541
2526
  }
542
2527
  rejectRequest(requestId, error) {
2528
+ return this.settleRequest(requestId, (request) => request.reject(error));
2529
+ }
2530
+ settleRequest(requestId, settle) {
2531
+ const now = Date.now();
2532
+ this.pruneAcceptedRequestIds(now);
543
2533
  const request = this.pendingRequests.get(requestId);
544
- if (request) {
545
- clearTimeout(request.timeoutId);
546
- this.pendingRequests.delete(requestId);
547
- request.reject(error);
2534
+ if (!request) {
2535
+ return this.acceptedRequestIds.has(requestId) ? "already_settled" : "unknown";
2536
+ }
2537
+ this.removePendingRequest(request);
2538
+ this.acceptedRequestIds.set(requestId, now);
2539
+ this.pruneAcceptedRequestIds(now);
2540
+ settle(request);
2541
+ return "accepted";
2542
+ }
2543
+ pruneAcceptedRequestIds(now) {
2544
+ for (const [requestId, acceptedAt] of this.acceptedRequestIds) {
2545
+ if (now - acceptedAt < ACCEPTED_REQUEST_TOMBSTONE_TTL_MS)
2546
+ break;
2547
+ this.acceptedRequestIds.delete(requestId);
2548
+ }
2549
+ while (this.acceptedRequestIds.size > MAX_ACCEPTED_REQUEST_TOMBSTONES) {
2550
+ const oldestRequestId = this.acceptedRequestIds.keys().next().value;
2551
+ if (oldestRequestId === void 0)
2552
+ break;
2553
+ this.acceptedRequestIds.delete(oldestRequestId);
2554
+ }
2555
+ }
2556
+ prunePendingCancellations(now) {
2557
+ for (const [requestId, cancellation] of this.pendingCancellations) {
2558
+ if (now - cancellation.createdAt < CANCELLATION_TOMBSTONE_TTL_MS)
2559
+ break;
2560
+ this.pendingCancellations.delete(requestId);
2561
+ }
2562
+ while (this.pendingCancellations.size > MAX_CANCELLATION_TOMBSTONES) {
2563
+ const oldestRequestId = this.pendingCancellations.keys().next().value;
2564
+ if (oldestRequestId === void 0)
2565
+ break;
2566
+ this.pendingCancellations.delete(oldestRequestId);
548
2567
  }
549
2568
  }
550
2569
  cleanupOldRequests() {
551
2570
  const now = Date.now();
552
- for (const [id, request] of this.pendingRequests.entries()) {
553
- if (now - request.timestamp > request.timeoutMs) {
554
- clearTimeout(request.timeoutId);
555
- this.pendingRequests.delete(id);
2571
+ for (const request of this.pendingRequests.values()) {
2572
+ if (now - request.timestamp > request.timeoutMs && this.removePendingRequest(request)) {
2573
+ this.notifyRequestCancelled(request, "timeout");
556
2574
  request.reject(new Error("Request timeout"));
557
2575
  }
558
2576
  }
559
2577
  }
560
2578
  clearAllPendingRequests() {
561
- for (const [, request] of this.pendingRequests.entries()) {
562
- clearTimeout(request.timeoutId);
2579
+ for (const request of Array.from(this.pendingRequests.values())) {
2580
+ this.removePendingRequest(request);
563
2581
  request.reject(new Error("Connection closed"));
564
2582
  }
565
- this.pendingRequests.clear();
566
- for (const waiter of Array.from(this.pendingPolls.values())) {
567
- this.settlePendingPoll(waiter, { kind: "bridge_closed" });
568
- }
2583
+ this.pendingCancellations.clear();
569
2584
  }
570
2585
  };
571
2586
 
@@ -3377,8 +5392,7 @@ var INTERNAL_RESULT_KEYS = /* @__PURE__ */ new Set([
3377
5392
  "pluginVariant",
3378
5393
  "pluginVersion",
3379
5394
  "requestId",
3380
- "serverVersion",
3381
- "versionMismatch"
5395
+ "serverVersion"
3382
5396
  ]);
3383
5397
  var TEXT_RESULT_TOOLS = /* @__PURE__ */ new Set(["get_roblox_docs"]);
3384
5398
  var SIDE_EFFECTING_READ_TOOLS = /* @__PURE__ */ new Set([
@@ -3591,9 +5605,9 @@ function createToolServer(options) {
3591
5605
  inputSchema: fromJsonSchema(publicDefinition.inputSchema),
3592
5606
  ...publicDefinition.outputSchema ? { outputSchema: fromJsonSchema(publicDefinition.outputSchema) } : {},
3593
5607
  annotations: publicDefinition.annotations
3594
- }, async (args) => {
5608
+ }, async (args, context) => {
3595
5609
  try {
3596
- const raw = await options.invoke(options.getTools(), definition.name, args);
5610
+ const raw = await options.invoke(options.getTools(), definition.name, args, { signal: context.mcpReq.signal });
3597
5611
  return normalizeToolResult(raw, options.era);
3598
5612
  } catch (error) {
3599
5613
  return normalizeToolResult({
@@ -3649,12 +5663,196 @@ function resolveAuthToken() {
3649
5663
  console.error(`[auth] Could not read/create ${filePath} (${err2 instanceof Error ? err2.message : err2}). Using an in-memory token for this process; set ROBLOX_STUDIO_AUTH_TOKEN to share one across sessions.`);
3650
5664
  return { token: randomBytes(32).toString("hex"), source: "file" };
3651
5665
  }
3652
- }
3653
- function tokensMatch(provided, expected) {
3654
- const a = createHash("sha256").update(provided).digest();
3655
- const b = createHash("sha256").update(expected).digest();
3656
- return timingSafeEqual(a, b);
3657
- }
5666
+ }
5667
+ function tokensMatch(provided, expected) {
5668
+ const a = createHash("sha256").update(provided).digest();
5669
+ const b = createHash("sha256").update(expected).digest();
5670
+ return timingSafeEqual(a, b);
5671
+ }
5672
+
5673
+ // ../core/dist/studio-transport.js
5674
+ var HEARTBEAT_INTERVAL_MS = 1e4;
5675
+ var MAX_ACTIVE_EVENT_STREAMS = 64;
5676
+ var SseStudioTransport = class {
5677
+ queue;
5678
+ streams = /* @__PURE__ */ new Map();
5679
+ unsubscribeRequestAvailable;
5680
+ unsubscribeSessionClosed;
5681
+ nextGeneration = 0;
5682
+ constructor(queue) {
5683
+ this.queue = queue;
5684
+ this.unsubscribeRequestAvailable = queue.onRequestAvailable((physicalSessionId) => {
5685
+ const stream = this.streams.get(physicalSessionId);
5686
+ if (stream)
5687
+ this.pump(stream);
5688
+ });
5689
+ this.unsubscribeSessionClosed = queue.onSessionClosed((route) => {
5690
+ if (route.logicalSessionId === route.physicalSessionId) {
5691
+ this.closePhysical(route.physicalSessionId);
5692
+ }
5693
+ });
5694
+ }
5695
+ get activeStreamCount() {
5696
+ return this.streams.size;
5697
+ }
5698
+ canOpen(physicalSessionId) {
5699
+ return this.streams.has(physicalSessionId) || this.streams.size < MAX_ACTIVE_EVENT_STREAMS;
5700
+ }
5701
+ open(physicalSessionId, sink, status) {
5702
+ if (!this.canOpen(physicalSessionId))
5703
+ return void 0;
5704
+ this.nextGeneration += 1;
5705
+ const claimOwner = `sse:${physicalSessionId}:${this.nextGeneration}`;
5706
+ const stream = {
5707
+ physicalSessionId,
5708
+ claimOwner,
5709
+ sink,
5710
+ status,
5711
+ closed: false,
5712
+ blocked: false,
5713
+ statusPending: true,
5714
+ onClose: () => this.closeStream(stream),
5715
+ onDrain: () => {
5716
+ if (stream.closed)
5717
+ return;
5718
+ stream.blocked = false;
5719
+ this.pump(stream);
5720
+ }
5721
+ };
5722
+ this.queue.setDeliveryActive(physicalSessionId, claimOwner, true);
5723
+ this.queue.updateInstanceActivity(physicalSessionId);
5724
+ const replaced = this.streams.get(physicalSessionId);
5725
+ if (replaced)
5726
+ this.closeStream(replaced, true);
5727
+ this.streams.set(physicalSessionId, stream);
5728
+ sink.on("close", stream.onClose);
5729
+ sink.on("error", stream.onClose);
5730
+ sink.on("drain", stream.onDrain);
5731
+ stream.heartbeatTimer = setInterval(() => {
5732
+ if (!stream.closed && !stream.blocked) {
5733
+ this.queue.updateInstanceActivity(physicalSessionId);
5734
+ stream.statusPending = true;
5735
+ this.pump(stream);
5736
+ if (!stream.blocked) {
5737
+ this.write(stream, { kind: "heartbeat", timestamp: Date.now() });
5738
+ }
5739
+ }
5740
+ }, HEARTBEAT_INTERVAL_MS);
5741
+ stream.heartbeatTimer.unref();
5742
+ this.pump(stream);
5743
+ return {
5744
+ physicalSessionId,
5745
+ close: () => this.closeStream(stream, true)
5746
+ };
5747
+ }
5748
+ refreshStatus(physicalSessionId) {
5749
+ if (physicalSessionId !== void 0) {
5750
+ const stream = this.streams.get(physicalSessionId);
5751
+ if (stream) {
5752
+ stream.lastStatusJson = void 0;
5753
+ stream.statusPending = true;
5754
+ this.pump(stream);
5755
+ }
5756
+ return;
5757
+ }
5758
+ for (const stream of this.streams.values()) {
5759
+ stream.lastStatusJson = void 0;
5760
+ stream.statusPending = true;
5761
+ this.pump(stream);
5762
+ }
5763
+ }
5764
+ closePhysical(physicalSessionId) {
5765
+ const stream = this.streams.get(physicalSessionId);
5766
+ if (stream)
5767
+ this.closeStream(stream, true);
5768
+ }
5769
+ close() {
5770
+ for (const stream of Array.from(this.streams.values())) {
5771
+ this.closeStream(stream, true);
5772
+ }
5773
+ this.unsubscribeRequestAvailable();
5774
+ this.unsubscribeSessionClosed();
5775
+ }
5776
+ pump(stream) {
5777
+ if (stream.closed || stream.blocked || this.streams.get(stream.physicalSessionId) !== stream)
5778
+ return;
5779
+ if (stream.statusPending) {
5780
+ stream.statusPending = false;
5781
+ let status;
5782
+ try {
5783
+ status = stream.status();
5784
+ } catch {
5785
+ this.closeStream(stream);
5786
+ return;
5787
+ }
5788
+ const statusJson = JSON.stringify(status);
5789
+ if (statusJson !== stream.lastStatusJson) {
5790
+ stream.lastStatusJson = statusJson;
5791
+ if (!this.write(stream, status))
5792
+ return;
5793
+ }
5794
+ }
5795
+ while (!stream.closed && !stream.blocked) {
5796
+ const cancellation = this.queue.claimNextCancellationForPhysical(stream.physicalSessionId, stream.claimOwner);
5797
+ if (!cancellation)
5798
+ break;
5799
+ if (!this.write(stream, { kind: "cancel", ...cancellation }))
5800
+ return;
5801
+ }
5802
+ while (!stream.closed && !stream.blocked) {
5803
+ const request = this.queue.claimNextRequestForPhysical(stream.physicalSessionId, stream.claimOwner);
5804
+ if (!request)
5805
+ return;
5806
+ const event = {
5807
+ kind: "request",
5808
+ requestId: request.requestId,
5809
+ logicalSessionId: request.logicalSessionId,
5810
+ target: request.target,
5811
+ endpoint: request.endpoint,
5812
+ data: request.data === void 0 ? null : request.data,
5813
+ remainingMs: request.remainingMs
5814
+ };
5815
+ if (!this.write(stream, event))
5816
+ return;
5817
+ }
5818
+ }
5819
+ write(stream, event) {
5820
+ if (stream.closed)
5821
+ return false;
5822
+ try {
5823
+ const writable = stream.sink.write(`data: ${JSON.stringify(event)}
5824
+
5825
+ `);
5826
+ if (!writable)
5827
+ stream.blocked = true;
5828
+ return writable;
5829
+ } catch {
5830
+ this.closeStream(stream);
5831
+ return false;
5832
+ }
5833
+ }
5834
+ closeStream(stream, endSink = false) {
5835
+ if (stream.closed)
5836
+ return;
5837
+ stream.closed = true;
5838
+ clearInterval(stream.heartbeatTimer);
5839
+ stream.sink.removeListener("close", stream.onClose);
5840
+ stream.sink.removeListener("error", stream.onClose);
5841
+ stream.sink.removeListener("drain", stream.onDrain);
5842
+ if (this.streams.get(stream.physicalSessionId) === stream) {
5843
+ this.streams.delete(stream.physicalSessionId);
5844
+ }
5845
+ this.queue.updateInstanceActivity(stream.physicalSessionId);
5846
+ this.queue.setDeliveryActive(stream.physicalSessionId, stream.claimOwner, false);
5847
+ this.queue.releaseDeliveryClaims(stream.claimOwner);
5848
+ if (endSink) {
5849
+ try {
5850
+ stream.sink.end();
5851
+ } catch {
5852
+ }
5853
+ }
5854
+ }
5855
+ };
3658
5856
 
3659
5857
  // ../core/dist/http-server.js
3660
5858
  function parseLineRange(lineRange) {
@@ -3713,7 +5911,7 @@ var TOOL_HANDLERS = {
3713
5911
  get_instance_properties: (tools, body) => tools.getInstanceProperties(body.instancePath, body.excludeSource, body.instance_id),
3714
5912
  get_project_structure: (tools, body) => tools.getProjectStructure(body.path, body.maxDepth, body.scriptsOnly, body.instance_id),
3715
5913
  set_properties: (tools, body) => tools.setProperties(body.instancePath, body.properties, body.instance_id),
3716
- grep_scripts: (tools, body) => tools.grepScripts(body.pattern, {
5914
+ grep_scripts: (tools, body, context) => tools.grepScripts(body.pattern, {
3717
5915
  caseSensitive: body.caseSensitive,
3718
5916
  usePattern: body.usePattern,
3719
5917
  contextLines: body.contextLines,
@@ -3722,7 +5920,7 @@ var TOOL_HANDLERS = {
3722
5920
  filesOnly: body.filesOnly,
3723
5921
  path: body.path,
3724
5922
  classFilter: body.classFilter
3725
- }, body.instance_id),
5923
+ }, body.instance_id, context?.signal),
3726
5924
  get_script_source: (tools, body) => {
3727
5925
  const { startLine, endLine } = optionalLineRange(body, "get_script_source");
3728
5926
  return tools.getScriptSource(body.instancePath, startLine, endLine, body.instance_id);
@@ -3812,26 +6010,26 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
3812
6010
  let lastMCPActivity = 0;
3813
6011
  let mcpServerStartTime = 0;
3814
6012
  const proxyInstances = /* @__PURE__ */ new Set();
3815
- const warnedVersionMismatches = /* @__PURE__ */ new Set();
3816
- let activePollEpoch = new AbortController();
3817
- activePollEpoch.abort();
6013
+ const rejectedVersionSessions = /* @__PURE__ */ new Set();
6014
+ const eventTransport = new SseStudioTransport(bridge);
6015
+ const eventStreamHandles = /* @__PURE__ */ new Set();
3818
6016
  const setMCPServerActive = (active) => {
3819
6017
  mcpServerActive = active;
3820
6018
  if (active) {
3821
- if (activePollEpoch.signal.aborted) {
3822
- activePollEpoch = new AbortController();
3823
- }
3824
6019
  mcpServerStartTime = Date.now();
3825
6020
  lastMCPActivity = Date.now();
3826
6021
  } else {
3827
- activePollEpoch.abort();
3828
6022
  mcpServerStartTime = 0;
3829
6023
  lastMCPActivity = 0;
3830
6024
  }
6025
+ eventTransport.refreshStatus();
3831
6026
  };
3832
6027
  const trackMCPActivity = () => {
3833
6028
  if (mcpServerActive) {
6029
+ const wasConnected = Date.now() - lastMCPActivity < 3e4;
3834
6030
  lastMCPActivity = Date.now();
6031
+ if (!wasConnected)
6032
+ eventTransport.refreshStatus();
3835
6033
  }
3836
6034
  };
3837
6035
  const isMCPServerActive = () => {
@@ -3839,6 +6037,18 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
3839
6037
  return false;
3840
6038
  return Date.now() - lastMCPActivity < 3e4;
3841
6039
  };
6040
+ const eventStatus = (physicalSessionId) => {
6041
+ const instance = bridge.getInstanceBySessionId(physicalSessionId);
6042
+ const knownInstance = instance?.physicalSessionId === physicalSessionId;
6043
+ return {
6044
+ kind: "status",
6045
+ knownInstance,
6046
+ mcpConnected: isMCPServerActive(),
6047
+ serverVersion: serverConfig?.version,
6048
+ pluginVersion: instance?.pluginVersion,
6049
+ pluginVariant: instance?.pluginVariant
6050
+ };
6051
+ };
3842
6052
  const isPluginConnected = () => {
3843
6053
  return bridge.getInstances().length > 0;
3844
6054
  };
@@ -3908,17 +6118,18 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
3908
6118
  pluginConnected: instances.length > 0,
3909
6119
  instanceCount: instances.length,
3910
6120
  instances: publicInstances,
3911
- versionMismatch: publicInstances.some((inst) => inst.versionMismatch),
3912
6121
  mcpServerActive: isMCPServerActive(),
3913
6122
  uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
3914
6123
  pendingRequests: bridge.getPendingRequestCount(),
3915
6124
  proxyInstanceCount: proxyInstances.size,
6125
+ activeEventStreams: eventTransport.activeStreamCount,
3916
6126
  streamableHttp: !!serverConfig
3917
6127
  });
3918
6128
  });
3919
6129
  app.post("/ready", (req, res) => {
3920
- const { pluginSessionId, instanceId, role, placeId, placeName, dataModelName, isRunning, pluginVersion, pluginVariant } = req.body;
6130
+ const { pluginSessionId, physicalSessionId, instanceId, role, placeId, placeName, dataModelName, isRunning, pluginVersion, pluginVariant } = req.body;
3921
6131
  const requestContext = {
6132
+ physicalSessionId: typeof physicalSessionId === "string" ? physicalSessionId : void 0,
3922
6133
  instanceId: typeof instanceId === "string" ? instanceId : void 0,
3923
6134
  role: typeof role === "string" ? role : void 0,
3924
6135
  placeId: typeof placeId === "number" ? placeId : void 0,
@@ -3928,12 +6139,15 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
3928
6139
  pluginVersion: typeof pluginVersion === "string" ? pluginVersion : void 0,
3929
6140
  pluginVariant: typeof pluginVariant === "string" ? pluginVariant : void 0
3930
6141
  };
3931
- if (!pluginSessionId || !instanceId || !role) {
3932
- const missingFields = [
3933
- !pluginSessionId ? "pluginSessionId" : void 0,
3934
- !instanceId ? "instanceId" : void 0,
3935
- !role ? "role" : void 0
3936
- ].filter((field) => !!field);
6142
+ const missingFields = [
6143
+ typeof pluginSessionId !== "string" || pluginSessionId === "" ? "pluginSessionId" : void 0,
6144
+ typeof physicalSessionId !== "string" || physicalSessionId === "" ? "physicalSessionId" : void 0,
6145
+ typeof instanceId !== "string" || instanceId === "" ? "instanceId" : void 0,
6146
+ typeof role !== "string" || role === "" ? "role" : void 0,
6147
+ typeof pluginVersion !== "string" || pluginVersion === "" ? "pluginVersion" : void 0,
6148
+ typeof pluginVariant !== "string" || pluginVariant === "" ? "pluginVariant" : void 0
6149
+ ].filter((field) => !!field);
6150
+ if (missingFields.length > 0) {
3937
6151
  res.status(400).json({
3938
6152
  success: false,
3939
6153
  error: "missing_ready_fields",
@@ -3943,19 +6157,71 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
3943
6157
  });
3944
6158
  return;
3945
6159
  }
6160
+ const serverVersion = serverConfig?.version;
6161
+ if (!serverVersion) {
6162
+ res.status(503).json({
6163
+ success: false,
6164
+ error: "server_version_unavailable",
6165
+ message: "The MCP server cannot accept Studio connections without a configured version.",
6166
+ request: requestContext
6167
+ });
6168
+ return;
6169
+ }
6170
+ if (pluginVersion !== serverVersion) {
6171
+ if (!rejectedVersionSessions.has(pluginSessionId)) {
6172
+ if (rejectedVersionSessions.size >= 256)
6173
+ rejectedVersionSessions.clear();
6174
+ rejectedVersionSessions.add(pluginSessionId);
6175
+ console.error(`[plugin-version-rejected] Studio plugin v${pluginVersion} (${pluginVariant}) does not match MCP server v${serverVersion} for ${instanceId}/${role}`);
6176
+ }
6177
+ res.status(426).json({
6178
+ success: false,
6179
+ error: "plugin_version_mismatch",
6180
+ message: `Studio plugin v${pluginVersion} does not match MCP server v${serverVersion}.`,
6181
+ pluginVersion,
6182
+ serverVersion,
6183
+ request: requestContext
6184
+ });
6185
+ return;
6186
+ }
6187
+ const isClientRole = role === "client" || /^client-[1-9]\d*$/.test(role);
6188
+ const isLogicalSession = physicalSessionId !== pluginSessionId;
6189
+ if (isLogicalSession && !isClientRole || !isLogicalSession && isClientRole || !isClientRole && role !== "edit" && role !== "server") {
6190
+ res.status(400).json({
6191
+ success: false,
6192
+ error: "invalid_session_topology",
6193
+ message: "Physical sessions must use the edit or server role; client roles must use a distinct physical server session.",
6194
+ request: requestContext
6195
+ });
6196
+ return;
6197
+ }
6198
+ if (isLogicalSession) {
6199
+ const physicalOwner = bridge.getInstanceBySessionId(physicalSessionId);
6200
+ const requestedInstanceId = typeof placeId === "number" && Number.isFinite(placeId) && placeId > 0 ? `place:${Math.trunc(placeId)}` : bridge.resolveInstanceId(instanceId);
6201
+ if (!physicalOwner || physicalOwner.pluginSessionId !== physicalSessionId || physicalOwner.physicalSessionId !== physicalSessionId || physicalOwner.role !== "server" || physicalOwner.instanceId !== requestedInstanceId) {
6202
+ res.status(409).json({
6203
+ success: false,
6204
+ error: "physical_session_unavailable",
6205
+ message: "A logical client requires a registered physical server session for the same Studio instance.",
6206
+ request: requestContext
6207
+ });
6208
+ return;
6209
+ }
6210
+ }
3946
6211
  let result;
3947
6212
  try {
3948
6213
  result = bridge.registerInstance({
3949
6214
  pluginSessionId,
6215
+ physicalSessionId,
3950
6216
  instanceId,
3951
6217
  role,
3952
6218
  placeId: typeof placeId === "number" ? placeId : 0,
3953
6219
  placeName: typeof placeName === "string" ? placeName : "",
3954
6220
  dataModelName: typeof dataModelName === "string" ? dataModelName : "",
3955
6221
  isRunning: !!isRunning,
3956
- pluginVersion: typeof pluginVersion === "string" ? pluginVersion : "",
3957
- pluginVariant: typeof pluginVariant === "string" ? pluginVariant : "unknown",
3958
- serverVersion: serverConfig?.version ?? ""
6222
+ pluginVersion,
6223
+ pluginVariant,
6224
+ serverVersion
3959
6225
  });
3960
6226
  } catch (err2) {
3961
6227
  res.status(500).json({
@@ -3976,17 +6242,12 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
3976
6242
  });
3977
6243
  return;
3978
6244
  }
3979
- const registered = bridge.getInstanceBySessionId(pluginSessionId);
3980
- if (registered?.versionMismatch && !warnedVersionMismatches.has(pluginSessionId)) {
3981
- warnedVersionMismatches.add(pluginSessionId);
3982
- console.error(`[version-mismatch] Studio plugin v${registered.pluginVersion} (${registered.pluginVariant}) does not match MCP server v${registered.serverVersion} for ${registered.instanceId}/${registered.role}`);
3983
- }
6245
+ eventTransport.refreshStatus(physicalSessionId);
3984
6246
  res.json({
3985
6247
  success: true,
3986
6248
  assignedRole: result.assignedRole,
3987
6249
  instanceId: result.instanceId,
3988
- serverVersion: serverConfig?.version,
3989
- versionMismatch: registered?.versionMismatch ?? false
6250
+ serverVersion
3990
6251
  });
3991
6252
  });
3992
6253
  app.post("/disconnect", (req, res) => {
@@ -4013,7 +6274,6 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
4013
6274
  instanceCount: instances.length,
4014
6275
  instances: publicInstances,
4015
6276
  serverVersion: serverConfig?.version,
4016
- versionMismatch: publicInstances.some((inst) => inst.versionMismatch),
4017
6277
  mcpServerActive: isMCPServerActive(),
4018
6278
  lastMCPActivity,
4019
6279
  uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0
@@ -4023,205 +6283,112 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
4023
6283
  const instances = bridge.getInstances();
4024
6284
  res.json({
4025
6285
  instances,
4026
- serverVersion: serverConfig?.version,
4027
- versionMismatch: instances.some((inst) => inst.versionMismatch)
6286
+ serverVersion: serverConfig?.version
4028
6287
  });
4029
6288
  });
4030
- app.get("/poll", async (req, res, next) => {
6289
+ app.get("/events", (req, res) => {
4031
6290
  const pluginSessionId = typeof req.query.pluginSessionId === "string" ? req.query.pluginSessionId : void 0;
4032
- const longPollRequested = req.query.pollMode === "long";
4033
- if (pluginSessionId) {
4034
- bridge.updateInstanceActivity(pluginSessionId);
4035
- }
4036
- const pollMetadata = () => {
4037
- const instance = pluginSessionId ? bridge.getInstanceBySessionId(pluginSessionId) : void 0;
4038
- return {
4039
- knownInstance: instance !== void 0,
4040
- pluginVersion: instance?.pluginVersion,
4041
- pluginVariant: instance?.pluginVariant,
4042
- versionMismatch: instance?.versionMismatch ?? false
4043
- };
4044
- };
4045
- let metadata = pollMetadata();
4046
- if (!isMCPServerActive()) {
4047
- res.status(503).json({
4048
- error: "MCP server not connected",
4049
- pluginConnected: true,
4050
- mcpConnected: false,
4051
- knownInstance: metadata.knownInstance,
4052
- serverVersion: serverConfig?.version,
4053
- pluginVersion: metadata.pluginVersion,
4054
- pluginVariant: metadata.pluginVariant,
4055
- versionMismatch: metadata.versionMismatch,
4056
- request: null
6291
+ if (!pluginSessionId) {
6292
+ res.status(400).json({
6293
+ error: "missing_plugin_session_id",
6294
+ message: "pluginSessionId is required"
4057
6295
  });
4058
6296
  return;
4059
6297
  }
4060
- if (!pluginSessionId || !metadata.knownInstance) {
4061
- res.json({
4062
- request: null,
4063
- mcpConnected: true,
4064
- pluginConnected: true,
4065
- knownInstance: false,
4066
- serverVersion: serverConfig?.version,
4067
- pluginVersion: metadata.pluginVersion,
4068
- pluginVariant: metadata.pluginVariant,
4069
- versionMismatch: metadata.versionMismatch,
4070
- proxyInstanceCount: proxyInstances.size,
4071
- pollMode: longPollRequested ? "long" : void 0
6298
+ const instance = bridge.getInstanceBySessionId(pluginSessionId);
6299
+ if (!instance) {
6300
+ res.status(404).json({
6301
+ error: "unknown_session",
6302
+ knownInstance: false
4072
6303
  });
4073
6304
  return;
4074
6305
  }
4075
- if (!longPollRequested) {
4076
- const instance = bridge.getInstanceBySessionId(pluginSessionId);
4077
- const pendingRequest = instance ? bridge.getPendingRequest(instance.instanceId, instance.role) : null;
4078
- metadata = pollMetadata();
4079
- res.json({
4080
- request: pendingRequest?.request ?? null,
4081
- requestId: pendingRequest?.requestId,
4082
- mcpConnected: true,
4083
- pluginConnected: true,
4084
- knownInstance: metadata.knownInstance,
4085
- serverVersion: serverConfig?.version,
4086
- pluginVersion: metadata.pluginVersion,
4087
- pluginVariant: metadata.pluginVariant,
4088
- versionMismatch: metadata.versionMismatch,
4089
- proxyInstanceCount: proxyInstances.size
6306
+ if (instance.physicalSessionId !== pluginSessionId) {
6307
+ res.status(409).json({
6308
+ error: "logical_session_has_no_event_stream",
6309
+ physicalSessionId: instance.physicalSessionId
4090
6310
  });
4091
6311
  return;
4092
6312
  }
4093
- const pollEpoch = activePollEpoch;
4094
- const pollController = new AbortController();
4095
- let socketClosed = false;
4096
- let mcpBecameInactive = false;
4097
- const detachListeners = () => {
4098
- req.removeListener("aborted", onSocketClosed);
4099
- res.removeListener("close", onSocketClosed);
4100
- res.removeListener("finish", onResponseFinished);
4101
- pollEpoch.signal.removeEventListener("abort", onMCPInactive);
4102
- };
4103
- const onSocketClosed = () => {
4104
- socketClosed = true;
4105
- pollController.abort();
4106
- detachListeners();
4107
- };
4108
- const onResponseFinished = () => {
4109
- detachListeners();
4110
- };
4111
- const onMCPInactive = () => {
4112
- mcpBecameInactive = true;
4113
- pollController.abort();
4114
- };
4115
- req.once("aborted", onSocketClosed);
4116
- res.once("close", onSocketClosed);
4117
- res.once("finish", onResponseFinished);
4118
- pollEpoch.signal.addEventListener("abort", onMCPInactive, { once: true });
4119
- if (req.aborted || res.destroyed) {
4120
- onSocketClosed();
4121
- } else if (pollEpoch.signal.aborted) {
4122
- onMCPInactive();
4123
- }
4124
- try {
4125
- const result = await bridge.waitForPendingRequest(pluginSessionId, {
4126
- signal: pollController.signal
4127
- });
4128
- if (socketClosed || res.destroyed) {
4129
- return;
4130
- }
4131
- metadata = pollMetadata();
4132
- if (result.kind === "request" && metadata.knownInstance) {
4133
- res.json({
4134
- request: result.request,
4135
- requestId: result.requestId,
4136
- mcpConnected: true,
4137
- pluginConnected: true,
4138
- knownInstance: true,
4139
- serverVersion: serverConfig?.version,
4140
- pluginVersion: metadata.pluginVersion,
4141
- pluginVariant: metadata.pluginVariant,
4142
- versionMismatch: metadata.versionMismatch,
4143
- proxyInstanceCount: proxyInstances.size,
4144
- pollMode: "long"
4145
- });
4146
- return;
4147
- }
4148
- if (mcpBecameInactive || pollEpoch.signal.aborted || !isMCPServerActive()) {
4149
- res.status(503).json({
4150
- error: "MCP server not connected",
4151
- pluginConnected: true,
4152
- mcpConnected: false,
4153
- knownInstance: metadata.knownInstance,
4154
- serverVersion: serverConfig?.version,
4155
- pluginVersion: metadata.pluginVersion,
4156
- pluginVariant: metadata.pluginVariant,
4157
- versionMismatch: metadata.versionMismatch,
4158
- request: null
4159
- });
4160
- return;
4161
- }
4162
- if (result.kind === "capacity") {
4163
- res.setHeader("Retry-After", "1");
4164
- res.status(503).json({
4165
- error: "Poll capacity reached",
4166
- code: "poll_capacity_reached",
4167
- pluginConnected: true,
4168
- mcpConnected: true,
4169
- knownInstance: metadata.knownInstance,
4170
- request: null
4171
- });
4172
- return;
4173
- }
4174
- res.json({
4175
- request: null,
4176
- mcpConnected: true,
4177
- pluginConnected: true,
4178
- knownInstance: metadata.knownInstance,
4179
- serverVersion: serverConfig?.version,
4180
- pluginVersion: metadata.pluginVersion,
4181
- pluginVariant: metadata.pluginVariant,
4182
- versionMismatch: metadata.versionMismatch,
4183
- proxyInstanceCount: proxyInstances.size,
4184
- pollMode: "long"
6313
+ if (!eventTransport.canOpen(pluginSessionId)) {
6314
+ res.setHeader("Retry-After", "1");
6315
+ res.status(503).json({
6316
+ error: "event_stream_capacity_reached",
6317
+ capacity: MAX_ACTIVE_EVENT_STREAMS
4185
6318
  });
4186
- } catch (error) {
4187
- detachListeners();
4188
- next(error);
6319
+ return;
6320
+ }
6321
+ bridge.updateInstanceActivity(pluginSessionId);
6322
+ res.status(200);
6323
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
6324
+ res.setHeader("Cache-Control", "no-cache, no-transform");
6325
+ res.setHeader("Connection", "keep-alive");
6326
+ res.setHeader("X-Accel-Buffering", "no");
6327
+ res.flushHeaders();
6328
+ const handle = eventTransport.open(pluginSessionId, res, () => eventStatus(pluginSessionId));
6329
+ if (!handle) {
6330
+ res.end();
6331
+ return;
4189
6332
  }
6333
+ eventStreamHandles.add(handle);
6334
+ res.once("close", () => eventStreamHandles.delete(handle));
4190
6335
  });
4191
6336
  app.post("/response", (req, res) => {
4192
6337
  const { requestId, response, error } = req.body;
4193
- if (error) {
4194
- bridge.rejectRequest(requestId, error);
4195
- } else {
4196
- bridge.resolveRequest(requestId, response);
6338
+ if (typeof requestId !== "string" || requestId.length === 0) {
6339
+ res.status(400).json({
6340
+ success: false,
6341
+ error: "invalid_request_id"
6342
+ });
6343
+ return;
4197
6344
  }
4198
- res.json({ success: true });
6345
+ const disposition = error !== void 0 ? bridge.rejectRequest(requestId, error) : bridge.resolveRequest(requestId, response);
6346
+ if (disposition === "unknown") {
6347
+ res.status(404).json({ success: false, disposition });
6348
+ return;
6349
+ }
6350
+ res.json({ success: true, disposition });
4199
6351
  });
4200
6352
  app.post("/proxy", async (req, res) => {
4201
- const { endpoint, data, targetInstanceId, targetRole, proxyInstanceId } = req.body;
6353
+ const { endpoint, data, targetInstanceId, targetRole, proxyInstanceId, timeoutMs } = req.body;
4202
6354
  if (!endpoint || !targetInstanceId || !targetRole) {
4203
6355
  res.status(400).json({ error: "endpoint, targetInstanceId, and targetRole are required" });
4204
6356
  return;
4205
6357
  }
6358
+ if (timeoutMs !== void 0 && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5)) {
6359
+ res.status(400).json({ error: "timeoutMs must be an integer between 1 and 300000" });
6360
+ return;
6361
+ }
4206
6362
  if (proxyInstanceId) {
4207
6363
  proxyInstances.add(proxyInstanceId);
4208
6364
  }
6365
+ const controller = new AbortController();
6366
+ const abort = () => controller.abort();
6367
+ req.once("aborted", abort);
6368
+ res.once("close", abort);
4209
6369
  try {
4210
- const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole);
6370
+ const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs, controller.signal);
4211
6371
  res.json({ response });
4212
- } catch (err2) {
4213
- res.status(500).json({ error: err2.message || "Proxy request failed" });
6372
+ } catch (error) {
6373
+ if (!res.headersSent && !res.destroyed) {
6374
+ res.status(500).json({
6375
+ error: error instanceof Error ? error.message : "Proxy request failed"
6376
+ });
6377
+ }
6378
+ } finally {
6379
+ req.removeListener("aborted", abort);
6380
+ res.removeListener("close", abort);
4214
6381
  }
4215
6382
  });
4216
6383
  const mcpHandler = serverConfig ? createToolHttpHandler({
4217
6384
  config: serverConfig,
4218
6385
  getTools: () => tools,
4219
6386
  allowedTools,
4220
- invoke: async (currentTools, name, args) => {
6387
+ invoke: async (currentTools, name, args, context) => {
4221
6388
  const handler = TOOL_HANDLERS[name];
4222
6389
  if (!handler)
4223
6390
  throw new Error(`Unknown tool: ${name}`);
4224
- return handler(currentTools, args);
6391
+ return handler(currentTools, args, context);
4225
6392
  }
4226
6393
  }) : void 0;
4227
6394
  const nodeMcpHandler = mcpHandler ? toNodeHandler(mcpHandler) : void 0;
@@ -4257,27 +6424,30 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
4257
6424
  app.isMCPServerActive = isMCPServerActive;
4258
6425
  app.trackMCPActivity = trackMCPActivity;
4259
6426
  app.closeMcpHandler = () => mcpHandler?.close();
6427
+ app.cleanup = async () => {
6428
+ for (const handle of eventStreamHandles)
6429
+ handle.close();
6430
+ eventStreamHandles.clear();
6431
+ eventTransport.close();
6432
+ await mcpHandler?.close();
6433
+ };
4260
6434
  return app;
4261
6435
  }
4262
- function listenWithRetry(app, host, startPort, maxAttempts = 5) {
4263
- return new Promise(async (resolve5, reject) => {
4264
- for (let i = 0; i < maxAttempts; i++) {
4265
- const port = startPort + i;
4266
- try {
4267
- const server = await bindPort(app, host, port);
4268
- resolve5({ server, port });
4269
- return;
4270
- } catch (err2) {
4271
- if (err2.code === "EADDRINUSE") {
4272
- console.error(`Port ${port} in use, trying next...`);
4273
- continue;
4274
- }
4275
- reject(err2);
4276
- return;
6436
+ async function listenWithRetry(app, host, startPort, maxAttempts = 5) {
6437
+ for (let i = 0; i < maxAttempts; i++) {
6438
+ const port = startPort + i;
6439
+ try {
6440
+ const server = await bindPort(app, host, port);
6441
+ return { server, port };
6442
+ } catch (error) {
6443
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "EADDRINUSE") {
6444
+ console.error(`Port ${port} in use, trying next...`);
6445
+ continue;
4277
6446
  }
6447
+ throw error;
4278
6448
  }
4279
- reject(new Error(`All ports ${startPort}-${startPort + maxAttempts - 1} are in use. Stop some MCP server instances and retry.`));
4280
- });
6449
+ }
6450
+ throw new Error(`All ports ${startPort}-${startPort + maxAttempts - 1} are in use. Stop some MCP server instances and retry.`);
4281
6451
  }
4282
6452
  function bindPort(app, host, port) {
4283
6453
  return new Promise((resolve5, reject) => {
@@ -4300,9 +6470,9 @@ var StudioHttpClient = class {
4300
6470
  constructor(bridge) {
4301
6471
  this.bridge = bridge;
4302
6472
  }
4303
- async request(endpoint, data, targetInstanceId, targetRole, timeoutMs) {
6473
+ async request(endpoint, data, targetInstanceId, targetRole, timeoutMs, signal) {
4304
6474
  try {
4305
- const response = await this.bridge.sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs);
6475
+ const response = await this.bridge.sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs, signal);
4306
6476
  return response;
4307
6477
  } catch (error) {
4308
6478
  if (error instanceof Error && error.message === "Request timeout") {
@@ -7106,6 +9276,7 @@ function rgbaToPng(rgba, width, height) {
7106
9276
  import * as fs3 from "fs";
7107
9277
  import * as path5 from "path";
7108
9278
  var MAX_INLINE_IMAGE_BYTES = 6e6;
9279
+ var MAX_MATRIX_IMAGE_BYTES = 8e6;
7109
9280
  var DEFAULT_ASSET_AUDIO_PREVIEWS = 3;
7110
9281
  var MAX_ASSET_AUDIO_PREVIEWS = 5;
7111
9282
  var MAX_INLINE_AUDIO_PREVIEW_BYTES = 3e6;
@@ -7116,6 +9287,8 @@ var MAX_SEARCH_ASSET_DESCRIPTION_LENGTH = 240;
7116
9287
  var ROBLOX_CREATOR_USER_ID = 1;
7117
9288
  var MAX_DEVICE_MATRIX_ENTRIES = 6;
7118
9289
  var MAX_NETWORK_PACKET_LOSS_PERCENT = 0.5;
9290
+ var GREP_SCRIPTS_TIMEOUT_MS = 12e4;
9291
+ var MAX_GREP_PATTERN_UTF8_BYTES = 4096;
7119
9292
  var STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL = "Studio Assistant Source Image";
7120
9293
  var CREATOR_STORE_SEARCH_TYPES = /* @__PURE__ */ new Set([
7121
9294
  "Audio",
@@ -7942,7 +10115,7 @@ var RobloxStudioTools = class {
7942
10115
  // ambiguous, missing, or asks for fanout on a non-fanout-capable tool —
7943
10116
  // the MCP transport layer surfaces it as a structured error result so
7944
10117
  // the LLM can recover via the embedded data.instances list.
7945
- async _callSingle(endpoint, data, target, instance_id, timeoutMs) {
10118
+ async _callSingle(endpoint, data, target, instance_id, timeoutMs, signal) {
7946
10119
  const r = this.bridge.resolveTarget({ instance_id, target });
7947
10120
  if (!r.ok)
7948
10121
  throw new RoutingFailure(r.error);
@@ -7956,7 +10129,10 @@ var RobloxStudioTools = class {
7956
10129
  }
7957
10130
  });
7958
10131
  }
7959
- return this.client.request(endpoint, data, r.targetInstanceId, r.targetRole, timeoutMs);
10132
+ if (signal === void 0) {
10133
+ return this.client.request(endpoint, data, r.targetInstanceId, r.targetRole, timeoutMs);
10134
+ }
10135
+ return this.client.request(endpoint, data, r.targetInstanceId, r.targetRole, timeoutMs, signal);
7960
10136
  }
7961
10137
  // Resolves which connected place a tool should target and whether a playtest
7962
10138
  // CLIENT peer is present on it. Used by capture/input to auto-route to the
@@ -8047,7 +10223,7 @@ var RobloxStudioTools = class {
8047
10223
  }
8048
10224
  _resolveDeviceSimulatorSingleTarget(target, instance_id, toolName) {
8049
10225
  const selectedTarget = target ?? "edit";
8050
- if (selectedTarget === "server" || selectedTarget === "all" || selectedTarget === "all-clients" || selectedTarget === "edit-proxy") {
10226
+ if (selectedTarget === "server" || selectedTarget === "all" || selectedTarget === "all-clients") {
8051
10227
  throw new Error(`${toolName} target must be "edit" or "client-N" (got: ${selectedTarget})`);
8052
10228
  }
8053
10229
  if (selectedTarget !== "edit" && !/^client-\d+$/.test(selectedTarget)) {
@@ -8085,7 +10261,7 @@ var RobloxStudioTools = class {
8085
10261
  }
8086
10262
  _resolveSimulationTargets(target, instance_id, toolName) {
8087
10263
  const selectedTarget = target ?? "edit-and-clients";
8088
- if (selectedTarget === "server" || selectedTarget === "all" || selectedTarget === "edit-proxy") {
10264
+ if (selectedTarget === "server" || selectedTarget === "all") {
8089
10265
  throw new Error(`${toolName} target must be "edit", "client-N", "all-clients", or "edit-and-clients" (got: ${selectedTarget})`);
8090
10266
  }
8091
10267
  const instanceId = this._resolveInstanceIdOnly(instance_id);
@@ -8456,14 +10632,17 @@ var RobloxStudioTools = class {
8456
10632
  ]
8457
10633
  };
8458
10634
  }
8459
- async grepScripts(pattern, options, instance_id) {
10635
+ async grepScripts(pattern, options, instance_id, signal) {
8460
10636
  if (!pattern) {
8461
10637
  throw new Error("Pattern is required for grep_scripts");
8462
10638
  }
10639
+ if (Buffer.byteLength(pattern, "utf8") > MAX_GREP_PATTERN_UTF8_BYTES) {
10640
+ throw new Error(`Pattern must not exceed ${MAX_GREP_PATTERN_UTF8_BYTES} UTF-8 bytes`);
10641
+ }
8463
10642
  const response = await this._callSingle("/api/grep-scripts", {
8464
10643
  pattern,
8465
10644
  ...options ?? {}
8466
- }, void 0, instance_id);
10645
+ }, void 0, instance_id, GREP_SCRIPTS_TIMEOUT_MS, signal);
8467
10646
  return {
8468
10647
  content: [
8469
10648
  {
@@ -8886,6 +11065,7 @@ var RobloxStudioTools = class {
8886
11065
  const entrySummaries = summary.entries;
8887
11066
  const content = [];
8888
11067
  const failures = [];
11068
+ let imageBudget = MAX_MATRIX_IMAGE_BYTES;
8889
11069
  try {
8890
11070
  for (let i = 0; i < matrixEntries.length; i++) {
8891
11071
  const entry = matrixEntries[i];
@@ -8903,8 +11083,10 @@ var RobloxStudioTools = class {
8903
11083
  entrySummary.applied = applied;
8904
11084
  if (settleMs > 0)
8905
11085
  await sleep(settleMs);
8906
- const capture = await this._captureViewportImage(resolved.instanceId, resolved.role, format, quality);
11086
+ const entryBudget = Math.max(1, Math.floor(imageBudget / (matrixEntries.length - i)));
11087
+ const capture = await this._captureViewportImage(resolved.instanceId, resolved.role, format, quality, entryBudget);
8907
11088
  if (capture.success) {
11089
+ imageBudget -= Math.ceil(capture.data.length * 3 / 4);
8908
11090
  entrySummary.screenshot = {
8909
11091
  width: capture.width,
8910
11092
  height: capture.height,
@@ -9001,7 +11183,7 @@ var RobloxStudioTools = class {
9001
11183
  content: [{ type: "text", text: JSON.stringify(response) }]
9002
11184
  };
9003
11185
  }
9004
- const targets = resolved.targets.filter((t) => t.targetRole !== "edit-proxy");
11186
+ const targets = resolved.targets;
9005
11187
  const originPeerReliable = targets.length > 0 ? await this._isMultiplayerTestRunning(targets[0].targetInstanceId) : false;
9006
11188
  const responses = await Promise.allSettled(targets.map(async (t) => {
9007
11189
  const r = await this.client.request("/api/get-runtime-logs", data, t.targetInstanceId, t.targetRole);
@@ -10646,7 +12828,7 @@ var RobloxStudioTools = class {
10646
12828
  const response = await this.client.request("/api/get-memory-breakdown", data, resolved.targetInstanceId, resolved.targetRole);
10647
12829
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
10648
12830
  }
10649
- const targets = resolved.targets.filter((t) => t.targetRole !== "edit-proxy");
12831
+ const targets = resolved.targets;
10650
12832
  const responses = await Promise.allSettled(targets.map(async (t) => ({
10651
12833
  peer: t.targetRole,
10652
12834
  result: await this.client.request("/api/get-memory-breakdown", data, t.targetInstanceId, t.targetRole)
@@ -10679,7 +12861,7 @@ var RobloxStudioTools = class {
10679
12861
  const response = await this.client.request("/api/get-scene-analysis", data, resolved.targetInstanceId, resolved.targetRole);
10680
12862
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
10681
12863
  }
10682
- const targets = resolved.targets.filter((t) => t.targetRole !== "edit-proxy");
12864
+ const targets = resolved.targets;
10683
12865
  const responses = await Promise.allSettled(targets.map(async (t) => ({
10684
12866
  peer: t.targetRole,
10685
12867
  result: await this.client.request("/api/get-scene-analysis", data, t.targetInstanceId, t.targetRole)
@@ -10803,7 +12985,7 @@ var RobloxStudioTools = class {
10803
12985
  }, tgt, instance_id);
10804
12986
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
10805
12987
  }
10806
- async _captureViewportImage(instanceId, targetRole, format, quality) {
12988
+ async _captureViewportImage(instanceId, targetRole, format, quality, maxBytes = MAX_INLINE_IMAGE_BYTES) {
10807
12989
  let response;
10808
12990
  if (targetRole.startsWith("client-")) {
10809
12991
  const begin = await this._callSingle("/api/capture-begin", {}, targetRole, instanceId);
@@ -10836,21 +13018,29 @@ var RobloxStudioTools = class {
10836
13018
  const { mimeType } = encoded;
10837
13019
  let usedQ = q;
10838
13020
  let note = "";
10839
- if (buffer.length > MAX_INLINE_IMAGE_BYTES) {
13021
+ if (buffer.length > maxBytes) {
10840
13022
  if (fmt === "png") {
10841
13023
  const mb = (buffer.length / 1048576).toFixed(1);
10842
13024
  return {
10843
13025
  success: false,
10844
- error: `PNG screenshot is ${mb}MB, over the ~${(MAX_INLINE_IMAGE_BYTES / 1048576).toFixed(0)}MB inline image limit. Use the default jpeg format (optionally with a "quality" value) or make the Studio window smaller for a lossless capture.`
13026
+ error: `PNG screenshot is ${mb}MB, over the ~${(maxBytes / 1048576).toFixed(1)}MB inline image limit. Use the default jpeg format (optionally with a "quality" value) or make the Studio window smaller for a lossless capture.`
10845
13027
  };
10846
13028
  }
10847
- while (buffer.length > MAX_INLINE_IMAGE_BYTES && usedQ > 25) {
13029
+ while (buffer.length > maxBytes && usedQ > 25) {
10848
13030
  usedQ = Math.max(25, usedQ - 20);
10849
13031
  buffer = encodeImageFromRgbaResponse(response, "jpeg", usedQ).buffer;
10850
13032
  }
13033
+ if (buffer.length > maxBytes) {
13034
+ return {
13035
+ success: false,
13036
+ error: `JPEG screenshot is still ${(buffer.length / 1048576).toFixed(1)}MB at q${usedQ}, over the ${(maxBytes / 1048576).toFixed(1)}MB inline budget. Make the Studio window smaller or capture fewer images per call.`
13037
+ };
13038
+ }
10851
13039
  note = ` \u2014 auto-reduced to q${usedQ} to fit the inline size limit; enlarge the Studio window or capture a smaller region for finer detail`;
10852
13040
  }
10853
- const message = `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image.`;
13041
+ const nativeW = response.nativeWidth ?? w;
13042
+ const nativeH = response.nativeHeight ?? h;
13043
+ const message = nativeW !== w || nativeH !== h ? `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}, downscaled from the ${nativeW}x${nativeH} viewport to fit transport limits. For simulate_mouse_input, multiply x read off this image by ${(nativeW / w).toFixed(4)} and y by ${(nativeH / h).toFixed(4)} to get viewport pixel coordinates ((0,0) at the top-left).` : `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image.`;
10854
13044
  return {
10855
13045
  success: true,
10856
13046
  width: w,
@@ -10878,7 +13068,8 @@ var RobloxStudioTools = class {
10878
13068
  height: capture.height,
10879
13069
  format: capture.format,
10880
13070
  mimeType: capture.mimeType,
10881
- ...capture.quality === void 0 ? {} : { quality: capture.quality }
13071
+ ...capture.quality === void 0 ? {} : { quality: capture.quality },
13072
+ message: capture.message
10882
13073
  })
10883
13074
  },
10884
13075
  {
@@ -10893,6 +13084,7 @@ var RobloxStudioTools = class {
10893
13084
 
10894
13085
  // ../core/dist/proxy-bridge-service.js
10895
13086
  import { randomUUID as randomUUID4 } from "crypto";
13087
+ var PROXY_RESPONSE_GRACE_MS = 5e3;
10896
13088
  var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
10897
13089
  primaryBaseUrl;
10898
13090
  authToken;
@@ -10968,9 +13160,20 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
10968
13160
  this.refreshTimer = void 0;
10969
13161
  }
10970
13162
  }
10971
- async sendRequest(endpoint, data, targetInstanceId, targetRole) {
13163
+ async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.proxyRequestTimeout, signal) {
13164
+ if (signal?.aborted)
13165
+ throw new Error("Request aborted");
10972
13166
  const controller = new AbortController();
10973
- const timeoutId = setTimeout(() => controller.abort(), this.proxyRequestTimeout);
13167
+ const effectiveTimeoutMs = Math.max(1, timeoutMs);
13168
+ let timedOut = false;
13169
+ const abortFromCaller = () => controller.abort();
13170
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
13171
+ if (signal?.aborted)
13172
+ controller.abort();
13173
+ const timeoutId = setTimeout(() => {
13174
+ timedOut = true;
13175
+ controller.abort();
13176
+ }, effectiveTimeoutMs + PROXY_RESPONSE_GRACE_MS);
10974
13177
  try {
10975
13178
  const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
10976
13179
  method: "POST",
@@ -10980,26 +13183,34 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
10980
13183
  data,
10981
13184
  targetInstanceId,
10982
13185
  targetRole,
10983
- proxyInstanceId: this.proxyInstanceId
13186
+ proxyInstanceId: this.proxyInstanceId,
13187
+ timeoutMs: effectiveTimeoutMs
10984
13188
  }),
10985
13189
  signal: controller.signal
10986
13190
  });
10987
- clearTimeout(timeoutId);
10988
13191
  if (!response.ok) {
10989
13192
  const body = await response.text();
10990
13193
  throw new Error(`Proxy request failed (${response.status}): ${body}`);
10991
13194
  }
10992
13195
  const result = await response.json();
10993
- if (result.error) {
13196
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
13197
+ throw new Error("Proxy returned an invalid response");
13198
+ }
13199
+ if ("error" in result && typeof result.error === "string" && result.error.length > 0) {
10994
13200
  throw new Error(result.error);
10995
13201
  }
10996
- return result.response;
10997
- } catch (err2) {
10998
- clearTimeout(timeoutId);
10999
- if (err2.name === "AbortError") {
13202
+ return "response" in result ? result.response : void 0;
13203
+ } catch (error) {
13204
+ const isAbortError = error instanceof Error ? error.name === "AbortError" : !!error && typeof error === "object" && "name" in error && error.name === "AbortError";
13205
+ if (isAbortError) {
13206
+ if (!timedOut && signal?.aborted)
13207
+ throw new Error("Request aborted");
11000
13208
  throw new Error("Proxy request timeout");
11001
13209
  }
11002
- throw err2;
13210
+ throw error;
13211
+ } finally {
13212
+ clearTimeout(timeoutId);
13213
+ signal?.removeEventListener("abort", abortFromCaller);
11003
13214
  }
11004
13215
  }
11005
13216
  cleanupOldRequests() {
@@ -11050,6 +13261,8 @@ var RobloxStudioMCPServer = class {
11050
13261
  console.error(`HTTP server listening on ${host}:${boundPort} for Studio plugin (primary mode)`);
11051
13262
  console.error(`Streamable HTTP MCP endpoint: http://localhost:${boundPort}/mcp`);
11052
13263
  } catch (error) {
13264
+ await primaryApp?.cleanup().catch(() => {
13265
+ });
11053
13266
  if (process.env.ROBLOX_STUDIO_REQUIRE_PRIMARY === "1") {
11054
13267
  console.error(`Port ${basePort} is unavailable and ROBLOX_STUDIO_REQUIRE_PRIMARY=1; refusing proxy mode`);
11055
13268
  throw error;
@@ -11083,33 +13296,21 @@ var RobloxStudioMCPServer = class {
11083
13296
  if (promotionInterval)
11084
13297
  clearInterval(promotionInterval);
11085
13298
  } catch {
13299
+ await candidateApp.cleanup().catch(() => {
13300
+ });
11086
13301
  }
11087
13302
  }, promotionIntervalMs);
11088
13303
  }
11089
- const LEGACY_PORT = 3002;
11090
- let legacyHandle;
11091
- let legacyApp;
11092
- if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
11093
- legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
11094
- try {
11095
- const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
11096
- legacyHandle = result.server;
11097
- console.error(`Legacy HTTP server also listening on ${host}:${LEGACY_PORT} for old plugins`);
11098
- legacyApp.setMCPServerActive(true);
11099
- } catch {
11100
- console.error(`Legacy port ${LEGACY_PORT} in use, skipping backward-compat listener`);
11101
- }
11102
- }
11103
13304
  const stdioHandle = serveStdio((context) => createToolServer({
11104
13305
  config: this.config,
11105
13306
  getTools: () => this.tools,
11106
13307
  allowedTools: this.allowedToolNames,
11107
13308
  era: context.era,
11108
- invoke: async (tools, name, args) => {
13309
+ invoke: async (tools, name, args, invocation) => {
11109
13310
  const handler = TOOL_HANDLERS[name];
11110
13311
  if (!handler)
11111
13312
  throw new Error(`Unknown tool: ${name}`);
11112
- return handler(tools, args);
13313
+ return handler(tools, args, invocation);
11113
13314
  }
11114
13315
  }), { onerror: (error) => console.error("[mcp:stdio]", error) });
11115
13316
  console.error(`${this.config.name} v${this.config.version} running on stdio`);
@@ -11121,8 +13322,6 @@ var RobloxStudioMCPServer = class {
11121
13322
  const activityInterval = setInterval(() => {
11122
13323
  if (primaryApp)
11123
13324
  primaryApp.trackMCPActivity();
11124
- if (legacyApp)
11125
- legacyApp.trackMCPActivity();
11126
13325
  if (bridgeMode === "primary" && primaryApp) {
11127
13326
  const pluginConnected = primaryApp.isPluginConnected();
11128
13327
  const mcpActive = primaryApp.isMCPServerActive();
@@ -11147,22 +13346,16 @@ var RobloxStudioMCPServer = class {
11147
13346
  if (promotionInterval)
11148
13347
  clearInterval(promotionInterval);
11149
13348
  primaryApp?.setMCPServerActive(false);
11150
- legacyApp?.setMCPServerActive(false);
11151
13349
  this.bridge.clearAllPendingRequests();
11152
13350
  if (this.bridge instanceof ProxyBridgeService) {
11153
13351
  this.bridge.stop();
11154
13352
  }
11155
13353
  await stdioHandle.close().catch(() => {
11156
13354
  });
11157
- await Promise.all([
11158
- primaryApp?.closeMcpHandler(),
11159
- legacyApp?.closeMcpHandler()
11160
- ]).catch(() => {
13355
+ await primaryApp?.cleanup().catch(() => {
11161
13356
  });
11162
13357
  if (httpHandle)
11163
13358
  httpHandle.close();
11164
- if (legacyHandle)
11165
- legacyHandle.close();
11166
13359
  process.exit(0);
11167
13360
  };
11168
13361
  process.on("SIGTERM", shutdown);
@@ -11564,37 +13757,44 @@ var TOOL_DEFINITIONS = [
11564
13757
  {
11565
13758
  name: "grep_scripts",
11566
13759
  category: "read",
11567
- description: "Use to locate text or Lua pattern matches across script sources.",
13760
+ description: "Use to search script sources.",
11568
13761
  inputSchema: {
11569
13762
  type: "object",
11570
13763
  properties: {
11571
13764
  pattern: {
11572
13765
  type: "string",
11573
- description: "Literal text, or a Lua pattern when usePattern is true."
13766
+ maxLength: 4096,
13767
+ description: "Literal or Lua pattern; max 4096 UTF-8 bytes."
11574
13768
  },
11575
13769
  caseSensitive: {
11576
13770
  type: "boolean",
11577
- description: "Literal match casing; patterns are always case-sensitive."
13771
+ description: "Lua patterns are always case-sensitive."
11578
13772
  },
11579
13773
  usePattern: {
11580
13774
  type: "boolean",
11581
- description: "Use Lua patterns with top-level | alternation; not PCRE."
13775
+ description: "Enable Lua pattern with top-level | (not PCRE)."
11582
13776
  },
11583
13777
  contextLines: {
11584
- type: "number",
11585
- description: "Lines before and after each match; defaults to 0."
13778
+ type: "integer",
13779
+ minimum: 0,
13780
+ maximum: 100,
13781
+ description: "Surrounding lines (0-100; default 0)."
11586
13782
  },
11587
13783
  maxResults: {
11588
- type: "number",
11589
- description: "Total match limit; defaults to 100."
13784
+ type: "integer",
13785
+ minimum: 1,
13786
+ maximum: 1e4,
13787
+ description: "Default 100."
11590
13788
  },
11591
13789
  maxResultsPerScript: {
11592
- type: "number",
11593
- description: "Match limit per script."
13790
+ type: "integer",
13791
+ minimum: 0,
13792
+ maximum: 1e4,
13793
+ description: "Per-script cap; 0 is unlimited."
11594
13794
  },
11595
13795
  filesOnly: {
11596
13796
  type: "boolean",
11597
- description: "Return only script paths; defaults to false."
13797
+ description: "Paths only; default false."
11598
13798
  },
11599
13799
  path: {
11600
13800
  type: "string",
@@ -12909,12 +15109,15 @@ var TOOL_DEFINITIONS = [
12909
15109
  var getReadOnlyTools = () => TOOL_DEFINITIONS.filter((t) => t.category === "read");
12910
15110
 
12911
15111
  // ../core/dist/install-plugin-helpers.js
12912
- import { existsSync as existsSync6, unlinkSync } from "fs";
15112
+ var import_saxes = __toESM(require_saxes(), 1);
15113
+ import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, statSync as statSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
12913
15114
  import { execSync } from "child_process";
12914
- import { join as join5 } from "path";
15115
+ import { randomUUID as randomUUID5 } from "crypto";
15116
+ import { basename as basename3, join as join5 } from "path";
12915
15117
  import { homedir as homedir4 } from "os";
15118
+ import { isUtf8 } from "buffer";
12916
15119
  var DEFAULT_PLUGIN_PORT = 58741;
12917
- var SERVER_URL_SETTING_KEY_PATTERN = /MCP_LAST_SUCCESSFUL_SERVER_URL_GLOBAL_V1|MCP_LAST_SUCCESSFUL_SERVER_URL_|MCP_SERVER_URL_/g;
15120
+ var SERVER_URL_SETTING_KEY_PATTERN = /MCP_LAST_SUCCESSFUL_SERVER_URL_GLOBAL_V1|MCP_LAST_SUCCESSFUL_SERVER_URL_/g;
12918
15121
  function configurePluginAssetForPort(source, rawPort = process.env.ROBLOX_STUDIO_PORT) {
12919
15122
  if (rawPort === void 0 || rawPort === "")
12920
15123
  return source;
@@ -12941,19 +15144,19 @@ function configurePluginAssetForPort(source, rawPort = process.env.ROBLOX_STUDIO
12941
15144
  function isWSL() {
12942
15145
  return getStudioPlatformCapabilities().isWsl;
12943
15146
  }
12944
- function getWindowsUserPluginsDir() {
15147
+ function getWindowsUserLocalAppData() {
12945
15148
  try {
12946
- const localAppData = execSync('cmd.exe /c "echo %LOCALAPPDATA%"', {
15149
+ const windowsPath = execSync('cmd.exe /c "echo %LOCALAPPDATA%"', {
12947
15150
  stdio: ["ignore", "pipe", "ignore"]
12948
15151
  }).toString().trim();
12949
- if (!localAppData || localAppData.includes("%"))
15152
+ if (!windowsPath || windowsPath.includes("%"))
12950
15153
  return null;
12951
- const linuxPath = execSync(`wslpath -u '${localAppData.replace(/'/g, "'\\''")}'`, {
15154
+ const linuxPath = execSync(`wslpath -u '${windowsPath.replace(/'/g, "'\\''")}'`, {
12952
15155
  stdio: ["ignore", "pipe", "ignore"]
12953
15156
  }).toString().trim();
12954
15157
  if (!linuxPath)
12955
15158
  return null;
12956
- return join5(linuxPath, "Roblox", "Plugins");
15159
+ return { linuxPath, windowsPath };
12957
15160
  } catch {
12958
15161
  return null;
12959
15162
  }
@@ -12965,13 +15168,78 @@ function getPluginsFolder() {
12965
15168
  return join5(process.env.LOCALAPPDATA || join5(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
12966
15169
  }
12967
15170
  if (isWSL()) {
12968
- const win = getWindowsUserPluginsDir();
12969
- if (win)
12970
- return win;
15171
+ const localAppData = getWindowsUserLocalAppData();
15172
+ if (localAppData)
15173
+ return join5(localAppData.linuxPath, "Roblox", "Plugins");
12971
15174
  console.warn("[install-plugin] WSL detected but could not resolve Windows %LOCALAPPDATA%. Falling back to ~/Documents/Roblox/Plugins/ - you will likely need to copy the rbxmx to /mnt/c/Users/<you>/AppData/Local/Roblox/Plugins/ manually. Set MCP_PLUGINS_DIR to skip detection.");
12972
15175
  }
12973
15176
  return join5(homedir4(), "Documents", "Roblox", "Plugins");
12974
15177
  }
15178
+ var STALE_TEST_PLUGINS_DIRECTORY = "RsmcpIsolatedPlugins";
15179
+ function pluginDirectorySettingPattern() {
15180
+ return /(<QDir\b[^>]*\bname="PluginsDir"[^>]*>)([\s\S]*?)(<\/QDir>)/gu;
15181
+ }
15182
+ function escapeXmlText(value) {
15183
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
15184
+ }
15185
+ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDirectory }) {
15186
+ if (!existsSync6(settingsPath))
15187
+ return false;
15188
+ if (studioPluginsDirectory === "") {
15189
+ throw new Error("studioPluginsDirectory must be a non-empty path.");
15190
+ }
15191
+ const contents = readFileSync6(settingsPath, "utf8");
15192
+ const matches = [...contents.matchAll(pluginDirectorySettingPattern())];
15193
+ if (matches.length !== 1) {
15194
+ throw new Error(`Expected exactly one Studio.PluginsDir QDir in ${settingsPath}; found ${matches.length}.`);
15195
+ }
15196
+ if (matches[0][2] !== STALE_TEST_PLUGINS_DIRECTORY)
15197
+ return false;
15198
+ const studioPath = escapeXmlText(studioPluginsDirectory);
15199
+ const updated = contents.replace(pluginDirectorySettingPattern(), (_match, open2, _value, close) => `${open2}${studioPath}${close}`);
15200
+ const temporaryPath = `${settingsPath}.rsmcp-install-${process.pid}-${randomUUID5()}.tmp`;
15201
+ try {
15202
+ writeFileSync3(temporaryPath, updated, {
15203
+ encoding: "utf8",
15204
+ mode: statSync3(settingsPath).mode
15205
+ });
15206
+ renameSync(temporaryPath, settingsPath);
15207
+ } finally {
15208
+ rmSync2(temporaryPath, { force: true });
15209
+ }
15210
+ return true;
15211
+ }
15212
+ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
15213
+ if (process.env.MCP_PLUGINS_DIR)
15214
+ return;
15215
+ let settingsPath;
15216
+ let defaultPluginsFolder;
15217
+ let studioPluginsDirectory;
15218
+ if (process.platform === "win32") {
15219
+ const localAppData = process.env.LOCALAPPDATA || join5(homedir4(), "AppData", "Local");
15220
+ defaultPluginsFolder = join5(localAppData, "Roblox", "Plugins");
15221
+ settingsPath = join5(localAppData, "Roblox", "GlobalSettings_13.xml");
15222
+ studioPluginsDirectory = defaultPluginsFolder.replaceAll("\\", "/");
15223
+ } else if (isWSL()) {
15224
+ const localAppData = getWindowsUserLocalAppData();
15225
+ if (!localAppData)
15226
+ return;
15227
+ defaultPluginsFolder = join5(localAppData.linuxPath, "Roblox", "Plugins");
15228
+ settingsPath = join5(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
15229
+ studioPluginsDirectory = [
15230
+ localAppData.windowsPath.replace(/[\\/]+$/u, ""),
15231
+ "Roblox",
15232
+ "Plugins"
15233
+ ].join("/").replaceAll("\\", "/");
15234
+ } else {
15235
+ return;
15236
+ }
15237
+ if (pluginsFolder !== defaultPluginsFolder)
15238
+ return;
15239
+ if (repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDirectory })) {
15240
+ log(`Restored Studio PluginsDir to ${studioPluginsDirectory}.`);
15241
+ }
15242
+ }
12975
15243
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
12976
15244
  const otherDest = join5(pluginsFolder, otherAssetName);
12977
15245
  if (!existsSync6(otherDest))
@@ -12991,12 +15259,190 @@ Only one MCP plugin variant should be present. If both variants are in the Studi
12991
15259
  Delete ${otherAssetName} manually or use the default CLI installer behavior to replace it.
12992
15260
  `);
12993
15261
  }
15262
+ var PLUGIN_INSTALL_LOCK_NAME = ".robloxstudio-mcp-plugin-install.lock";
15263
+ function errorCode(error) {
15264
+ return error.code;
15265
+ }
15266
+ function acquirePluginInstallLock(pluginsFolder, warn) {
15267
+ const lockPath = join5(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
15268
+ const ownerPath = join5(lockPath, "owner.json");
15269
+ const owner = {
15270
+ pid: process.pid,
15271
+ token: randomUUID5(),
15272
+ createdAt: Date.now()
15273
+ };
15274
+ try {
15275
+ mkdirSync4(lockPath);
15276
+ } catch (error) {
15277
+ if (errorCode(error) !== "EEXIST")
15278
+ throw error;
15279
+ throw new Error(`Another Studio plugin installation is already in progress: ${lockPath}. If no installer is running, remove that lock directory and retry.`);
15280
+ }
15281
+ try {
15282
+ writeFileSync3(ownerPath, `${JSON.stringify(owner)}
15283
+ `, {
15284
+ encoding: "utf8",
15285
+ flag: "wx"
15286
+ });
15287
+ } catch (error) {
15288
+ try {
15289
+ rmSync2(lockPath, { recursive: true, force: true });
15290
+ } catch (cleanupError) {
15291
+ warn(`[install-plugin] Could not clean failed install lock ${lockPath}: ${cleanupError}`);
15292
+ }
15293
+ throw error;
15294
+ }
15295
+ return () => {
15296
+ try {
15297
+ const currentOwner = JSON.parse(readFileSync6(ownerPath, "utf8"));
15298
+ if (currentOwner.pid === owner.pid && currentOwner.token === owner.token) {
15299
+ rmSync2(lockPath, { recursive: true, force: true });
15300
+ }
15301
+ } catch (error) {
15302
+ if (errorCode(error) !== "ENOENT") {
15303
+ warn(`[install-plugin] Could not release install lock ${lockPath}: ${error}`);
15304
+ }
15305
+ }
15306
+ };
15307
+ }
15308
+ function assertAssetFileName(name, field) {
15309
+ if (name === "" || basename3(name) !== name) {
15310
+ throw new Error(`${field} must be a file name, received ${JSON.stringify(name)}`);
15311
+ }
15312
+ }
15313
+ function embeddedPluginValue(source, name, assetName) {
15314
+ const pattern = new RegExp(`\\blocal\\s+${name}\\s*=\\s*"([^"]+)"\\s*;?`, "g");
15315
+ const matches = [...source.matchAll(pattern)];
15316
+ if (matches.length !== 1) {
15317
+ throw new Error(`${assetName} must contain exactly one embedded ${name}; found ${matches.length}.`);
15318
+ }
15319
+ return matches[0][1];
15320
+ }
15321
+ var PLUGIN_SOURCE_CLASSES = {
15322
+ Script: true,
15323
+ ModuleScript: true,
15324
+ LocalScript: true
15325
+ };
15326
+ function parsePluginScriptSources(source, assetName) {
15327
+ if (!isUtf8(source)) {
15328
+ throw new Error(`${assetName} is not valid UTF-8 Roblox XML.`);
15329
+ }
15330
+ const text = source.toString("utf8");
15331
+ const elementStack = [];
15332
+ const scriptSources = [];
15333
+ let rootName;
15334
+ let currentSource;
15335
+ const parser = new import_saxes.SaxesParser({ fragment: false, xmlns: false });
15336
+ parser.on("doctype", () => {
15337
+ throw new Error("DOCTYPE declarations are not allowed in plugin artifacts.");
15338
+ });
15339
+ parser.on("opentag", (tag) => {
15340
+ if (elementStack.length === 0)
15341
+ rootName = tag.name;
15342
+ if (currentSource === void 0 && tag.name === "string" && tag.attributes.name === "Source") {
15343
+ const nearestItem = [...elementStack].reverse().find((element) => element.name === "Item");
15344
+ const sourceClass = nearestItem?.attributes.class;
15345
+ if (sourceClass !== void 0 && PLUGIN_SOURCE_CLASSES[sourceClass] === true) {
15346
+ currentSource = { depth: elementStack.length, text: "" };
15347
+ }
15348
+ }
15349
+ elementStack.push({ name: tag.name, attributes: tag.attributes });
15350
+ });
15351
+ parser.on("text", (value) => {
15352
+ if (currentSource !== void 0)
15353
+ currentSource.text += value;
15354
+ });
15355
+ parser.on("cdata", (value) => {
15356
+ if (currentSource !== void 0)
15357
+ currentSource.text += value;
15358
+ });
15359
+ parser.on("closetag", (tag) => {
15360
+ if (currentSource !== void 0 && tag.name === "string" && elementStack.length === currentSource.depth + 1) {
15361
+ scriptSources.push(currentSource.text);
15362
+ currentSource = void 0;
15363
+ }
15364
+ elementStack.pop();
15365
+ });
15366
+ try {
15367
+ parser.write(text).close();
15368
+ } catch (error) {
15369
+ const detail = error instanceof Error ? error.message : String(error);
15370
+ throw new Error(`${assetName} is not well-formed Roblox XML: ${detail}`, {
15371
+ cause: error
15372
+ });
15373
+ }
15374
+ if (rootName !== "roblox" || scriptSources.length === 0) {
15375
+ throw new Error(`${assetName} is not a Roblox XML model containing a Script source.`);
15376
+ }
15377
+ return scriptSources;
15378
+ }
15379
+ function assertPluginAssetIdentity(source, assetName, expectedVersion, expectedVariant) {
15380
+ const pluginSource = parsePluginScriptSources(source, assetName).join("\n");
15381
+ const actualVersion = embeddedPluginValue(pluginSource, "CURRENT_VERSION", assetName);
15382
+ if (actualVersion !== expectedVersion) {
15383
+ throw new Error(`${assetName} embeds version ${actualVersion}; expected ${expectedVersion}.`);
15384
+ }
15385
+ const actualVariant = embeddedPluginValue(pluginSource, "PLUGIN_VARIANT", assetName);
15386
+ if (actualVariant !== expectedVariant) {
15387
+ throw new Error(`${assetName} embeds variant ${actualVariant}; expected ${expectedVariant}.`);
15388
+ }
15389
+ }
15390
+ function installedFileMatches(source, destination) {
15391
+ if (!existsSync6(destination))
15392
+ return false;
15393
+ if (!lstatSync(destination).isFile())
15394
+ return false;
15395
+ const installed = readFileSync6(destination);
15396
+ return installed.length === source.length && installed.equals(source);
15397
+ }
15398
+ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source, expectedVersion, expectedVariant, rawPort, replaceVariant = true, log = console.log, warn = console.warn }) {
15399
+ assertAssetFileName(assetName, "assetName");
15400
+ assertAssetFileName(otherAssetName, "otherAssetName");
15401
+ if (assetName === otherAssetName) {
15402
+ throw new Error("assetName and otherAssetName must identify different files.");
15403
+ }
15404
+ const configured = configurePluginAssetForPort(source, rawPort);
15405
+ assertPluginAssetIdentity(configured, assetName, expectedVersion, expectedVariant);
15406
+ mkdirSync4(pluginsFolder, { recursive: true });
15407
+ const destination = join5(pluginsFolder, assetName);
15408
+ const releaseLock = acquirePluginInstallLock(pluginsFolder, warn);
15409
+ try {
15410
+ let installed = false;
15411
+ if (!installedFileMatches(configured, destination)) {
15412
+ const staged = join5(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID5()}.tmp`);
15413
+ try {
15414
+ writeFileSync3(staged, configured, { flag: "wx" });
15415
+ renameSync(staged, destination);
15416
+ installed = true;
15417
+ } finally {
15418
+ try {
15419
+ unlinkSync(staged);
15420
+ } catch (error) {
15421
+ if (errorCode(error) !== "ENOENT") {
15422
+ warn(`[install-plugin] Could not remove staging file ${staged}: ${error}`);
15423
+ }
15424
+ }
15425
+ }
15426
+ }
15427
+ handleVariantConflict({
15428
+ pluginsFolder,
15429
+ otherAssetName,
15430
+ replace: replaceVariant,
15431
+ log,
15432
+ warn
15433
+ });
15434
+ repairDefaultStudioPluginDirectorySetting(pluginsFolder, log);
15435
+ return { destination, installed };
15436
+ } finally {
15437
+ releaseLock();
15438
+ }
15439
+ }
12994
15440
 
12995
15441
  // src/index.ts
12996
15442
  import { createRequire } from "module";
12997
15443
 
12998
15444
  // src/install-plugin.ts
12999
- import { createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
15445
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
13000
15446
  import { dirname as dirname5, join as join6 } from "path";
13001
15447
  import { fileURLToPath } from "url";
13002
15448
  import { get } from "https";
@@ -13005,6 +15451,7 @@ var ASSET_NAME = "MCPInspectorPlugin.rbxmx";
13005
15451
  var OTHER_VARIANT = "MCPPlugin.rbxmx";
13006
15452
  var TIMEOUT_MS = 3e4;
13007
15453
  var MAX_REDIRECTS = 5;
15454
+ var MAX_ASSET_BYTES = 64 * 1024 * 1024;
13008
15455
  function httpsGet(url) {
13009
15456
  return new Promise((resolve5, reject) => {
13010
15457
  const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp-inspector" } }, resolve5);
@@ -13014,36 +15461,33 @@ function httpsGet(url) {
13014
15461
  });
13015
15462
  });
13016
15463
  }
13017
- async function download(url, dest, redirects = 0) {
15464
+ async function download(url, redirects = 0) {
13018
15465
  const res = await httpsGet(url);
13019
15466
  if (res.statusCode === 301 || res.statusCode === 302) {
13020
15467
  if (redirects >= MAX_REDIRECTS) throw new Error(`Too many redirects (max ${MAX_REDIRECTS})`);
13021
15468
  const location = res.headers.location;
13022
15469
  if (!location) throw new Error("Redirect with no location header");
13023
- return download(location, dest, redirects + 1);
15470
+ for await (const _chunk of res) {
15471
+ }
15472
+ return download(location, redirects + 1);
13024
15473
  }
13025
15474
  if (res.statusCode !== 200) {
13026
15475
  throw new Error(`Download failed: HTTP ${res.statusCode}`);
13027
15476
  }
13028
- return new Promise((resolve5, reject) => {
13029
- const file = createWriteStream(dest);
13030
- const cleanup = (err2) => {
13031
- file.close(() => {
13032
- try {
13033
- unlinkSync2(dest);
13034
- } catch {
13035
- }
13036
- reject(err2);
13037
- });
13038
- };
13039
- res.pipe(file);
13040
- file.on("finish", () => {
13041
- file.close();
13042
- resolve5();
13043
- });
13044
- file.on("error", cleanup);
13045
- res.on("error", cleanup);
13046
- });
15477
+ const chunks = [];
15478
+ let totalBytes = 0;
15479
+ for await (const chunk of res) {
15480
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
15481
+ totalBytes += bytes.length;
15482
+ if (totalBytes > MAX_ASSET_BYTES) {
15483
+ res.destroy();
15484
+ throw new Error(
15485
+ `${ASSET_NAME} download exceeds the ${MAX_ASSET_BYTES}-byte limit.`
15486
+ );
15487
+ }
15488
+ chunks.push(bytes);
15489
+ }
15490
+ return Buffer.concat(chunks, totalBytes);
13047
15491
  }
13048
15492
  async function fetchJson(url) {
13049
15493
  const res = await httpsGet(url);
@@ -13056,24 +15500,6 @@ async function fetchJson(url) {
13056
15500
  }
13057
15501
  return JSON.parse(Buffer.concat(chunks).toString());
13058
15502
  }
13059
- function prepareInstall({
13060
- replaceVariant,
13061
- log,
13062
- warn
13063
- }) {
13064
- const pluginsFolder = getPluginsFolder();
13065
- if (!existsSync7(pluginsFolder)) {
13066
- mkdirSync4(pluginsFolder, { recursive: true });
13067
- }
13068
- handleVariantConflict({
13069
- pluginsFolder,
13070
- otherAssetName: OTHER_VARIANT,
13071
- replace: replaceVariant,
13072
- log,
13073
- warn
13074
- });
13075
- return pluginsFolder;
13076
- }
13077
15503
  function bundledAssetPath() {
13078
15504
  const currentDir = dirname5(fileURLToPath(import.meta.url));
13079
15505
  const candidates = [
@@ -13088,47 +15514,36 @@ function resolvePluginAssetPath(sourcePath) {
13088
15514
  }
13089
15515
  function packageVersion() {
13090
15516
  const currentDir = dirname5(fileURLToPath(import.meta.url));
13091
- const pkg = JSON.parse(readFileSync6(join6(currentDir, "..", "package.json"), "utf8"));
15517
+ const pkg = JSON.parse(readFileSync7(join6(currentDir, "..", "package.json"), "utf8"));
13092
15518
  if (!pkg.version) {
13093
15519
  throw new Error("Package version not found");
13094
15520
  }
13095
15521
  return pkg.version;
13096
15522
  }
13097
- function bundledPluginVersion(source) {
13098
- const match = readFileSync6(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
13099
- return match ? match[1] : null;
13100
- }
13101
- function assertBundledPluginVersion(source) {
13102
- const expected = packageVersion();
13103
- const actual = bundledPluginVersion(source);
13104
- if (actual !== expected) {
13105
- throw new Error(
13106
- `Bundled ${ASSET_NAME} version ${actual ?? "unknown"} does not match package version ${expected}. Run npm run build:plugin:inspector before starting with --auto-install-plugin.`
13107
- );
13108
- }
13109
- }
13110
- function filesMatch(expected, actualPath) {
13111
- if (!existsSync7(actualPath)) return false;
13112
- const actual = readFileSync6(actualPath);
13113
- return expected.length === actual.length && expected.equals(actual);
13114
- }
13115
15523
  async function installBundledPlugin(options = {}) {
13116
15524
  const log = options.log ?? console.log;
13117
15525
  const warn = options.warn ?? console.warn;
13118
15526
  const replaceVariant = options.replaceVariant ?? true;
13119
- const source = resolvePluginAssetPath(options.sourcePath);
13120
- if (!source) {
15527
+ const sourcePath = resolvePluginAssetPath(options.sourcePath);
15528
+ if (!sourcePath) {
13121
15529
  throw new Error(
13122
15530
  `Bundled ${ASSET_NAME} not found. Run npm run build:plugin:inspector in this worktree first.`
13123
15531
  );
13124
15532
  }
13125
- assertBundledPluginVersion(source);
13126
- const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
13127
- const dest = join6(pluginsFolder, ASSET_NAME);
13128
- const configured = configurePluginAssetForPort(readFileSync6(source));
13129
- if (filesMatch(configured, dest)) return;
13130
- writeFileSync3(dest, configured);
13131
- log(`Installed ${ASSET_NAME} to ${dest}`);
15533
+ const result = await installPluginAsset({
15534
+ pluginsFolder: getPluginsFolder(),
15535
+ assetName: ASSET_NAME,
15536
+ otherAssetName: OTHER_VARIANT,
15537
+ source: readFileSync7(sourcePath),
15538
+ expectedVersion: packageVersion(),
15539
+ expectedVariant: "inspector",
15540
+ replaceVariant,
15541
+ log,
15542
+ warn
15543
+ });
15544
+ if (result.installed) {
15545
+ log(`Installed ${ASSET_NAME} to ${result.destination}`);
15546
+ }
13132
15547
  }
13133
15548
  async function installPlugin(options = {}) {
13134
15549
  const replaceVariant = options.replaceVariant ?? true;
@@ -13138,17 +15553,23 @@ async function installPlugin(options = {}) {
13138
15553
  if (options.sourcePath !== void 0 && !bundled) {
13139
15554
  throw new Error(`Plugin asset not found at explicit path ${options.sourcePath}.`);
13140
15555
  }
13141
- const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
13142
15556
  if (bundled) {
13143
- assertBundledPluginVersion(bundled);
13144
- const dest2 = join6(pluginsFolder, ASSET_NAME);
13145
- const configured2 = configurePluginAssetForPort(readFileSync6(bundled));
13146
- if (filesMatch(configured2, dest2)) {
15557
+ const result2 = await installPluginAsset({
15558
+ pluginsFolder: getPluginsFolder(),
15559
+ assetName: ASSET_NAME,
15560
+ otherAssetName: OTHER_VARIANT,
15561
+ source: readFileSync7(bundled),
15562
+ expectedVersion: packageVersion(),
15563
+ expectedVariant: "inspector",
15564
+ replaceVariant,
15565
+ log,
15566
+ warn
15567
+ });
15568
+ if (result2.installed) {
15569
+ log(`Installed bundled ${ASSET_NAME} to ${result2.destination}`);
15570
+ } else {
13147
15571
  log(`${ASSET_NAME} already installed.`);
13148
- return;
13149
15572
  }
13150
- writeFileSync3(dest2, configured2);
13151
- log(`Installed bundled ${ASSET_NAME} to ${dest2}`);
13152
15573
  return;
13153
15574
  }
13154
15575
  log("Fetching latest release...");
@@ -13157,13 +15578,24 @@ async function installPlugin(options = {}) {
13157
15578
  if (!asset) {
13158
15579
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
13159
15580
  }
13160
- const dest = join6(pluginsFolder, ASSET_NAME);
13161
15581
  log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
13162
- await download(asset.browser_download_url, dest);
13163
- const downloaded = readFileSync6(dest);
13164
- const configured = configurePluginAssetForPort(downloaded);
13165
- if (configured !== downloaded) writeFileSync3(dest, configured);
13166
- log(`Installed to ${dest}`);
15582
+ const downloaded = await download(asset.browser_download_url);
15583
+ const result = await installPluginAsset({
15584
+ pluginsFolder: getPluginsFolder(),
15585
+ assetName: ASSET_NAME,
15586
+ otherAssetName: OTHER_VARIANT,
15587
+ source: downloaded,
15588
+ expectedVersion: release.tag_name.replace(/^v/, ""),
15589
+ expectedVariant: "inspector",
15590
+ replaceVariant,
15591
+ log,
15592
+ warn
15593
+ });
15594
+ if (result.installed) {
15595
+ log(`Installed to ${result.destination}`);
15596
+ } else {
15597
+ log(`${ASSET_NAME} already installed.`);
15598
+ }
13167
15599
  }
13168
15600
 
13169
15601
  // src/index.ts
@@ -13201,3 +15633,32 @@ if (installBundledOnly || installWithFallback) {
13201
15633
  process.exit(1);
13202
15634
  });
13203
15635
  }
15636
+ /*! Bundled license information:
15637
+
15638
+ xmlchars/xml/1.0/ed5.js:
15639
+ (**
15640
+ * Character classes and associated utilities for the 5th edition of XML 1.0.
15641
+ *
15642
+ * @author Louis-Dominique Dubeau
15643
+ * @license MIT
15644
+ * @copyright Louis-Dominique Dubeau
15645
+ *)
15646
+
15647
+ xmlchars/xml/1.1/ed2.js:
15648
+ (**
15649
+ * Character classes and associated utilities for the 2nd edition of XML 1.1.
15650
+ *
15651
+ * @author Louis-Dominique Dubeau
15652
+ * @license MIT
15653
+ * @copyright Louis-Dominique Dubeau
15654
+ *)
15655
+
15656
+ xmlchars/xmlns/1.0/ed3.js:
15657
+ (**
15658
+ * Character class utilities for XML NS 1.0 edition 3.
15659
+ *
15660
+ * @author Louis-Dominique Dubeau
15661
+ * @license MIT
15662
+ * @copyright Louis-Dominique Dubeau
15663
+ *)
15664
+ */