@princeofscale/bloxforge-inspector 2.20.2 → 3.0.0

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
@@ -14,6 +14,111 @@ var __export = (target, all) => {
14
14
  __defProp(target, name, { get: all[name], enumerable: true });
15
15
  };
16
16
 
17
+ // ../core/dist/request-journal.js
18
+ import * as fs from "fs";
19
+ import * as os from "os";
20
+ import * as path from "path";
21
+ function defaultRequestJournalPath() {
22
+ if (process.env.NODE_ENV === "test")
23
+ return void 0;
24
+ const configured = process.env.BLOXFORGE_JOURNAL_PATH?.trim();
25
+ if (configured === "off" || configured === "0")
26
+ return void 0;
27
+ return configured || path.join(os.homedir(), ".bloxforge", "bridge-journal.json");
28
+ }
29
+ var DEFAULT_STATUS_TTL_MS, MAX_STATUSES, MAX_RECEIPTS, RECEIPT_TTL_MS, COMPACT_INTERVAL, RequestJournal;
30
+ var init_request_journal = __esm({
31
+ "../core/dist/request-journal.js"() {
32
+ "use strict";
33
+ DEFAULT_STATUS_TTL_MS = 60 * 60 * 1e3;
34
+ MAX_STATUSES = 1e3;
35
+ MAX_RECEIPTS = 200;
36
+ RECEIPT_TTL_MS = 5 * 60 * 1e3;
37
+ COMPACT_INTERVAL = 50;
38
+ RequestJournal = class {
39
+ filePath;
40
+ persistCount = 0;
41
+ constructor(filePath) {
42
+ this.filePath = filePath;
43
+ }
44
+ load() {
45
+ try {
46
+ const raw = fs.readFileSync(this.filePath, "utf8");
47
+ const parsed = JSON.parse(raw);
48
+ if (!parsed || typeof parsed !== "object")
49
+ return void 0;
50
+ if (!Array.isArray(parsed.statuses) || !Array.isArray(parsed.pending))
51
+ return void 0;
52
+ if (parsed.version === 1) {
53
+ return {
54
+ version: 2,
55
+ savedAt: parsed.savedAt,
56
+ statuses: parsed.statuses,
57
+ pending: parsed.pending,
58
+ completionReceipts: []
59
+ };
60
+ }
61
+ if (parsed.version === 2 && Array.isArray(parsed.completionReceipts)) {
62
+ return parsed;
63
+ }
64
+ return void 0;
65
+ } catch {
66
+ this.backupCorruptFile();
67
+ return void 0;
68
+ }
69
+ }
70
+ save(statuses, pending, completionReceipts = []) {
71
+ const directory = path.dirname(this.filePath);
72
+ fs.mkdirSync(directory, { recursive: true, mode: 448 });
73
+ const snapshot = {
74
+ version: 2,
75
+ savedAt: Date.now(),
76
+ statuses,
77
+ pending,
78
+ completionReceipts
79
+ };
80
+ const temporary = `${this.filePath}.${process.pid}.tmp`;
81
+ fs.writeFileSync(temporary, JSON.stringify(snapshot), { mode: 384 });
82
+ fs.renameSync(temporary, this.filePath);
83
+ this.persistCount++;
84
+ if (this.persistCount >= COMPACT_INTERVAL) {
85
+ this.persistCount = 0;
86
+ }
87
+ }
88
+ /**
89
+ * Compact a snapshot in-place: enforce retention TTL and entry limits.
90
+ * Returns a new compacted snapshot.
91
+ */
92
+ static compact(snapshot, now = Date.now()) {
93
+ let statuses = snapshot.statuses.filter((s) => now - s.updatedAt < DEFAULT_STATUS_TTL_MS);
94
+ if (statuses.length > MAX_STATUSES) {
95
+ statuses = statuses.slice(statuses.length - MAX_STATUSES);
96
+ }
97
+ let receipts = snapshot.completionReceipts.filter((r) => now - r.completedAt < RECEIPT_TTL_MS);
98
+ if (receipts.length > MAX_RECEIPTS) {
99
+ receipts = receipts.slice(receipts.length - MAX_RECEIPTS);
100
+ }
101
+ return {
102
+ ...snapshot,
103
+ savedAt: now,
104
+ statuses,
105
+ completionReceipts: receipts
106
+ };
107
+ }
108
+ /** Back up a corrupt journal file so data is not silently lost. */
109
+ backupCorruptFile() {
110
+ try {
111
+ if (!fs.existsSync(this.filePath))
112
+ return;
113
+ const backup = `${this.filePath}.corrupt.${Date.now()}`;
114
+ fs.copyFileSync(this.filePath, backup);
115
+ } catch {
116
+ }
117
+ }
118
+ };
119
+ }
120
+ });
121
+
17
122
  // ../core/dist/bridge-service.js
18
123
  import { randomUUID } from "crypto";
19
124
  function toPublic(inst) {
@@ -56,10 +161,11 @@ function publishedInstanceId(placeId) {
56
161
  return void 0;
57
162
  return `place:${Math.trunc(placeId)}`;
58
163
  }
59
- var RoutingFailure, MCP_PROTOCOL_VERSION, STALE_INSTANCE_MS, INSTANCE_ALIAS_TTL_MS, DEFAULT_REQUEST_TIMEOUT_MS, HEAVY_REQUEST_TIMEOUT_FLOOR_MS, HEAVY_ENDPOINTS, BridgeService;
164
+ var RoutingFailure, RequestOutcomeUnknownError, MCP_PROTOCOL_VERSION, STALE_INSTANCE_MS, INSTANCE_ALIAS_TTL_MS, DEFAULT_REQUEST_TIMEOUT_MS, HEAVY_REQUEST_TIMEOUT_FLOOR_MS, HEAVY_ENDPOINTS, BridgeService;
60
165
  var init_bridge_service = __esm({
61
166
  "../core/dist/bridge-service.js"() {
62
167
  "use strict";
168
+ init_request_journal();
63
169
  RoutingFailure = class extends Error {
64
170
  routingError;
65
171
  constructor(routingError) {
@@ -68,7 +174,16 @@ var init_bridge_service = __esm({
68
174
  this.routingError = routingError;
69
175
  }
70
176
  };
71
- MCP_PROTOCOL_VERSION = 1;
177
+ RequestOutcomeUnknownError = class extends Error {
178
+ requestId;
179
+ outcome = "unknown";
180
+ constructor(requestId, endpoint, timeout) {
181
+ super(`Request timeout after ${timeout}ms on ${endpoint}. Request ${requestId} outcome unknown. The plugin may still be running heavy code \u2014 the work can succeed even though this call gave up.`);
182
+ this.requestId = requestId;
183
+ this.name = "RequestOutcomeUnknownError";
184
+ }
185
+ };
186
+ MCP_PROTOCOL_VERSION = 3;
72
187
  STALE_INSTANCE_MS = envStaleInstanceMs();
73
188
  INSTANCE_ALIAS_TTL_MS = 5 * 60 * 1e3;
74
189
  DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
@@ -86,6 +201,89 @@ var init_bridge_service = __esm({
86
201
  // with a small TTL without depending on wall-clock env vars. Production
87
202
  // callers leave this at the module-level STALE_INSTANCE_MS default.
88
203
  staleInstanceMs = STALE_INSTANCE_MS;
204
+ serverEpoch = randomUUID();
205
+ journal;
206
+ requestStatuses = /* @__PURE__ */ new Map();
207
+ sessionTokens = /* @__PURE__ */ new Map();
208
+ // sessionToken -> pluginSessionId
209
+ transportCounters = {
210
+ completed: 0,
211
+ outcomeUnknown: 0,
212
+ deliveryRetries: 0,
213
+ cancelled: 0,
214
+ timeouts: 0
215
+ };
216
+ transportLatencies = [];
217
+ constructor(journal) {
218
+ if (typeof journal === "string") {
219
+ this.journal = new RequestJournal(journal);
220
+ } else if (journal) {
221
+ this.journal = journal;
222
+ } else {
223
+ const path9 = defaultRequestJournalPath();
224
+ if (path9)
225
+ this.journal = new RequestJournal(path9);
226
+ }
227
+ if (this.journal) {
228
+ try {
229
+ const snapshot = this.journal.load();
230
+ if (snapshot) {
231
+ const { statuses, pending } = snapshot;
232
+ for (const s of statuses) {
233
+ if (s.state === "delivered" || s.state === "started") {
234
+ s.state = "outcome_unknown";
235
+ s.updatedAt = Date.now();
236
+ }
237
+ this.requestStatuses.set(s.requestId, s);
238
+ }
239
+ for (const p of pending) {
240
+ if (p.state !== "queued")
241
+ continue;
242
+ this.pendingRequests.set(p.id, {
243
+ id: p.id,
244
+ endpoint: p.endpoint,
245
+ data: p.data,
246
+ targetInstanceId: p.targetInstanceId,
247
+ targetRole: p.targetRole,
248
+ timestamp: p.timestamp,
249
+ inFlight: false,
250
+ deliveryAttempt: p.deliveryAttempt,
251
+ resolve: () => {
252
+ },
253
+ reject: () => {
254
+ },
255
+ timeoutId: setTimeout(() => {
256
+ }, 0)
257
+ });
258
+ }
259
+ }
260
+ } catch (e) {
261
+ }
262
+ }
263
+ }
264
+ enableJournal() {
265
+ const path9 = defaultRequestJournalPath();
266
+ if (path9)
267
+ this.journal = new RequestJournal(path9);
268
+ }
269
+ persistJournal() {
270
+ if (!this.journal)
271
+ return;
272
+ this.journal.save(Array.from(this.requestStatuses.entries()).map(([id, s]) => ({ requestId: id, ...s })), Array.from(this.pendingRequests.values()).map((req) => ({
273
+ id: req.id,
274
+ endpoint: req.endpoint,
275
+ data: req.data,
276
+ targetInstanceId: req.targetInstanceId,
277
+ targetRole: req.targetRole,
278
+ timestamp: req.timestamp,
279
+ state: this.requestStatuses.get(req.id)?.state ?? "queued",
280
+ deliveryAttempt: req.deliveryAttempt ?? 0
281
+ })), []);
282
+ }
283
+ updateRequestStatus(requestId, status) {
284
+ const existing = this.requestStatuses.get(requestId) ?? { state: "queued", serverEpoch: this.serverEpoch, deliveryAttempt: 0, updatedAt: Date.now() };
285
+ this.requestStatuses.set(requestId, { ...existing, ...status, updatedAt: Date.now() });
286
+ }
89
287
  canonicalInstanceId(instanceId, placeId) {
90
288
  return publishedInstanceId(placeId) ?? instanceId;
91
289
  }
@@ -212,30 +410,53 @@ var init_bridge_service = __esm({
212
410
  lastActivity: Date.now(),
213
411
  connectedAt: prior?.connectedAt ?? Date.now()
214
412
  });
215
- return { ok: true, assignedRole, instanceId };
413
+ const sessionToken = randomUUID();
414
+ this.sessionTokens.set(sessionToken, pluginSessionId);
415
+ return { ok: true, assignedRole, instanceId, sessionToken };
416
+ }
417
+ authenticatePlugin(pluginSessionId, sessionToken) {
418
+ const expectedId = this.sessionTokens.get(sessionToken);
419
+ return expectedId === pluginSessionId;
216
420
  }
217
421
  unregisterInstance(pluginSessionId, reason = "unknown") {
218
- const removed = this.instances.get(pluginSessionId);
219
- this.instances.delete(pluginSessionId);
220
- if (!removed)
422
+ const instance = this.instances.get(pluginSessionId);
423
+ if (!instance)
221
424
  return;
222
- this.recentDisconnects.unshift({
425
+ this.instances.delete(pluginSessionId);
426
+ this.recentDisconnects.push({
223
427
  pluginSessionId,
224
- instanceId: removed.instanceId,
225
- role: removed.role,
428
+ instanceId: instance.instanceId,
429
+ role: instance.role,
226
430
  reason,
227
431
  disconnectedAt: Date.now(),
228
- lastActivity: removed.lastActivity
432
+ lastActivity: instance.lastActivity
229
433
  });
230
- this.recentDisconnects.length = Math.min(this.recentDisconnects.length, 10);
231
- for (const [id, req] of this.pendingRequests.entries()) {
232
- const stillHasHandler = Array.from(this.instances.values()).some((i) => i.instanceId === req.targetInstanceId && i.role === req.targetRole);
434
+ if (this.recentDisconnects.length > 50)
435
+ this.recentDisconnects.shift();
436
+ for (const [id, request] of this.pendingRequests.entries()) {
437
+ const stillHasHandler = Array.from(this.instances.values()).some((i) => i.instanceId === request.targetInstanceId && i.role === request.targetRole);
438
+ const status = this.requestStatuses.get(id);
439
+ const wasAssignedToThis = request.assignedPluginSessionId === pluginSessionId;
233
440
  if (!stillHasHandler) {
234
- clearTimeout(req.timeoutId);
441
+ clearTimeout(request.timeoutId);
235
442
  this.pendingRequests.delete(id);
236
- req.reject(new Error(`Target (${req.targetInstanceId}, ${req.targetRole}) disconnected`));
443
+ if (status?.state === "delivered" || status?.state === "started") {
444
+ this.updateRequestStatus(id, { state: "outcome_unknown" });
445
+ request.reject(new RequestOutcomeUnknownError(id, request.endpoint, this.requestTimeout));
446
+ } else {
447
+ this.updateRequestStatus(id, { state: "failed", error: "Target disconnected" });
448
+ request.reject(new Error(`Target (${request.targetInstanceId}, ${request.targetRole}) disconnected`));
449
+ }
450
+ } else {
451
+ if (wasAssignedToThis && (status?.state === "delivered" || status?.state === "started")) {
452
+ clearTimeout(request.timeoutId);
453
+ this.pendingRequests.delete(id);
454
+ this.updateRequestStatus(id, { state: "outcome_unknown" });
455
+ request.reject(new RequestOutcomeUnknownError(id, request.endpoint, this.requestTimeout));
456
+ }
237
457
  }
238
458
  }
459
+ this.persistJournal();
239
460
  }
240
461
  getInstances() {
241
462
  return Array.from(this.instances.values());
@@ -290,6 +511,29 @@ var init_bridge_service = __esm({
290
511
  }
291
512
  }
292
513
  }
514
+ async lookupRequestStatus(requestId) {
515
+ const status = this.requestStatuses.get(requestId);
516
+ if (!status)
517
+ return void 0;
518
+ return { requestId, ...status };
519
+ }
520
+ getRequestStatus(requestId) {
521
+ const status = this.requestStatuses.get(requestId);
522
+ if (!status)
523
+ return void 0;
524
+ return { requestId, ...status };
525
+ }
526
+ getTransportDiagnostics() {
527
+ return {
528
+ completed: this.transportCounters.completed,
529
+ queueDepth: this.pendingRequests.size,
530
+ deliveryRetries: this.transportCounters.deliveryRetries,
531
+ timeouts: this.transportCounters.timeouts,
532
+ outcomeUnknown: this.transportCounters.outcomeUnknown,
533
+ cancelled: this.transportCounters.cancelled,
534
+ averageLatencyMs: this.transportLatencies.length > 0 ? this.transportLatencies.reduce((a, b) => a + b, 0) / this.transportLatencies.length : 0
535
+ };
536
+ }
293
537
  cleanupStaleInstances() {
294
538
  const now = Date.now();
295
539
  for (const [id, inst] of this.instances.entries()) {
@@ -419,11 +663,14 @@ var init_bridge_service = __esm({
419
663
  async sendRequest(endpoint, data, targetInstanceId, targetRole) {
420
664
  const requestId = randomUUID();
421
665
  const effectiveTimeout = resolveRequestTimeout(endpoint, this.requestTimeout);
422
- return new Promise((resolve6, reject) => {
666
+ return new Promise((resolve7, reject) => {
423
667
  const timeoutId = setTimeout(() => {
424
668
  if (this.pendingRequests.has(requestId)) {
425
669
  this.pendingRequests.delete(requestId);
426
- reject(new Error(`Request timeout after ${effectiveTimeout}ms on ${endpoint}. The plugin may still be running heavy code \u2014 the work can succeed even though this call gave up. Raise MCP_REQUEST_TIMEOUT_MS for big scripts.`));
670
+ this.updateRequestStatus(requestId, { state: "outcome_unknown" });
671
+ this.persistJournal();
672
+ this.transportCounters.timeouts++;
673
+ reject(new RequestOutcomeUnknownError(requestId, endpoint, effectiveTimeout));
427
674
  }
428
675
  }, effectiveTimeout);
429
676
  const request = {
@@ -434,11 +681,13 @@ var init_bridge_service = __esm({
434
681
  targetRole,
435
682
  timestamp: Date.now(),
436
683
  inFlight: false,
437
- resolve: resolve6,
684
+ resolve: resolve7,
438
685
  reject,
439
686
  timeoutId
440
687
  };
441
688
  this.pendingRequests.set(requestId, request);
689
+ this.updateRequestStatus(requestId, { state: "queued" });
690
+ this.persistJournal();
442
691
  for (const instance of this.instances.values()) {
443
692
  if (instance.instanceId === targetInstanceId && instance.role === targetRole) {
444
693
  for (const notify of this.requestNotifiers)
@@ -452,9 +701,28 @@ var init_bridge_service = __esm({
452
701
  if (!instance)
453
702
  return null;
454
703
  this.updateInstanceActivity(pluginSessionId);
455
- return this.getPendingRequest(instance.instanceId, instance.role);
704
+ return this.getPendingRequest(instance.instanceId, instance.role, pluginSessionId);
456
705
  }
457
- getPendingRequest(callerInstanceId, callerRole) {
706
+ getPendingRequest(callerInstanceId, callerRole, pluginSessionId) {
707
+ let inFlightMutations = 0;
708
+ let inFlightReads = 0;
709
+ for (const request of this.pendingRequests.values()) {
710
+ if (request.targetInstanceId !== callerInstanceId)
711
+ continue;
712
+ if (request.targetRole !== callerRole)
713
+ continue;
714
+ const status = this.requestStatuses.get(request.id);
715
+ if (request.inFlight && status?.state === "delivered" && Date.now() - status.updatedAt >= 1e4) {
716
+ request.inFlight = false;
717
+ this.updateRequestStatus(request.id, { state: "queued" });
718
+ }
719
+ if (request.inFlight) {
720
+ if (this.isMutation(request.endpoint))
721
+ inFlightMutations++;
722
+ else
723
+ inFlightReads++;
724
+ }
725
+ }
458
726
  let oldestRequest = null;
459
727
  for (const request of this.pendingRequests.values()) {
460
728
  if (request.targetInstanceId !== callerInstanceId)
@@ -463,14 +731,33 @@ var init_bridge_service = __esm({
463
731
  continue;
464
732
  if (request.inFlight)
465
733
  continue;
734
+ const isMutationReq = this.isMutation(request.endpoint);
735
+ if (isMutationReq && inFlightMutations >= 1)
736
+ continue;
737
+ if (!isMutationReq && inFlightReads >= 4)
738
+ continue;
466
739
  if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
467
740
  oldestRequest = request;
468
741
  }
469
742
  }
470
743
  if (oldestRequest) {
471
744
  oldestRequest.inFlight = true;
745
+ oldestRequest.deliveryAttempt = (oldestRequest.deliveryAttempt ?? 0) + 1;
746
+ oldestRequest.assignedPluginSessionId = pluginSessionId;
747
+ const leaseToken = randomUUID();
748
+ this.updateRequestStatus(oldestRequest.id, {
749
+ state: "delivered",
750
+ serverEpoch: this.serverEpoch,
751
+ deliveryAttempt: oldestRequest.deliveryAttempt,
752
+ leaseToken
753
+ });
754
+ this.persistJournal();
472
755
  return {
473
756
  requestId: oldestRequest.id,
757
+ serverEpoch: this.serverEpoch,
758
+ pluginSessionId: pluginSessionId ?? "",
759
+ deliveryAttempt: oldestRequest.deliveryAttempt,
760
+ leaseToken,
474
761
  request: {
475
762
  endpoint: oldestRequest.endpoint,
476
763
  data: oldestRequest.data
@@ -481,8 +768,11 @@ var init_bridge_service = __esm({
481
768
  }
482
769
  releasePendingRequest(requestId) {
483
770
  const request = this.pendingRequests.get(requestId);
484
- if (request)
771
+ if (request) {
485
772
  request.inFlight = false;
773
+ this.updateRequestStatus(requestId, { state: "queued" });
774
+ this.persistJournal();
775
+ }
486
776
  }
487
777
  releasePendingRequestsForSession(pluginSessionId) {
488
778
  const instance = this.instances.get(pluginSessionId);
@@ -491,24 +781,84 @@ var init_bridge_service = __esm({
491
781
  for (const request of this.pendingRequests.values()) {
492
782
  if (request.targetInstanceId === instance.instanceId && request.targetRole === instance.role) {
493
783
  request.inFlight = false;
784
+ request.assignedPluginSessionId = void 0;
785
+ this.updateRequestStatus(request.id, { state: "queued" });
494
786
  }
495
787
  }
788
+ this.persistJournal();
789
+ }
790
+ reconcilePluginReceipts(pluginSessionId, serverEpoch, receipts) {
791
+ if (serverEpoch !== this.serverEpoch)
792
+ return 0;
793
+ const instance = this.instances.get(pluginSessionId);
794
+ if (!instance)
795
+ return 0;
796
+ let reconciled = 0;
797
+ for (const receipt of receipts) {
798
+ const status = this.requestStatuses.get(receipt.requestId);
799
+ if (status && status.state === "outcome_unknown") {
800
+ status.state = "completed";
801
+ status.updatedAt = receipt.completedAt || Date.now();
802
+ status.response = { reconciled: true, digest: receipt.responseDigest };
803
+ reconciled++;
804
+ }
805
+ }
806
+ if (reconciled > 0) {
807
+ this.persistJournal();
808
+ }
809
+ return reconciled;
810
+ }
811
+ acknowledgeRequest(requestId) {
812
+ const status = this.requestStatuses.get(requestId);
813
+ if (!status)
814
+ return false;
815
+ this.updateRequestStatus(requestId, { state: "started" });
816
+ this.persistJournal();
817
+ return true;
496
818
  }
497
819
  resolveRequest(requestId, response) {
498
820
  const request = this.pendingRequests.get(requestId);
821
+ this.updateRequestStatus(requestId, { state: "completed", response, error: void 0 });
822
+ this.transportCounters.completed++;
499
823
  if (request) {
500
824
  clearTimeout(request.timeoutId);
501
825
  this.pendingRequests.delete(requestId);
502
826
  request.resolve(response);
503
827
  }
828
+ this.persistJournal();
504
829
  }
505
830
  rejectRequest(requestId, error) {
506
831
  const request = this.pendingRequests.get(requestId);
832
+ this.updateRequestStatus(requestId, { state: "failed", error });
507
833
  if (request) {
508
834
  clearTimeout(request.timeoutId);
509
835
  this.pendingRequests.delete(requestId);
510
836
  request.reject(error);
511
837
  }
838
+ this.persistJournal();
839
+ }
840
+ resolveFencedRequest(requestId, response, fence) {
841
+ const status = this.requestStatuses.get(requestId);
842
+ const pending = this.pendingRequests.get(requestId);
843
+ if (!status || status.serverEpoch !== fence.serverEpoch || status.deliveryAttempt !== fence.deliveryAttempt || status.leaseToken !== fence.leaseToken || status.state === "completed" || status.state === "failed" || pending?.assignedPluginSessionId !== fence.pluginSessionId)
844
+ return false;
845
+ this.resolveRequest(requestId, response);
846
+ return true;
847
+ }
848
+ rejectFencedRequest(requestId, error, fence) {
849
+ const status = this.requestStatuses.get(requestId);
850
+ const pending = this.pendingRequests.get(requestId);
851
+ if (!status || status.serverEpoch !== fence.serverEpoch || status.deliveryAttempt !== fence.deliveryAttempt || status.leaseToken !== fence.leaseToken || status.state === "completed" || status.state === "failed" || pending?.assignedPluginSessionId !== fence.pluginSessionId)
852
+ return false;
853
+ this.rejectRequest(requestId, error);
854
+ return true;
855
+ }
856
+ acknowledgeFencedRequest(requestId, fence) {
857
+ const status = this.requestStatuses.get(requestId);
858
+ const pending = this.pendingRequests.get(requestId);
859
+ if (!status || status.serverEpoch !== fence.serverEpoch || status.deliveryAttempt !== fence.deliveryAttempt || status.leaseToken !== fence.leaseToken || status.state === "completed" || status.state === "failed" || pending?.assignedPluginSessionId !== fence.pluginSessionId)
860
+ return false;
861
+ return this.acknowledgeRequest(requestId);
512
862
  }
513
863
  cleanupOldRequests() {
514
864
  const now = Date.now();
@@ -527,6 +877,48 @@ var init_bridge_service = __esm({
527
877
  }
528
878
  this.pendingRequests.clear();
529
879
  }
880
+ cancelRequest(requestId) {
881
+ const request = this.pendingRequests.get(requestId);
882
+ if (!request)
883
+ return false;
884
+ const status = this.requestStatuses.get(requestId);
885
+ if (status?.state === "started") {
886
+ return false;
887
+ }
888
+ clearTimeout(request.timeoutId);
889
+ this.pendingRequests.delete(requestId);
890
+ this.updateRequestStatus(requestId, { state: "cancelled", error: "Request cancelled before execution" });
891
+ this.transportCounters.cancelled++;
892
+ request.reject(new Error(`Request ${requestId} cancelled`));
893
+ this.persistJournal();
894
+ return true;
895
+ }
896
+ getCancellationEvents(pluginSessionId) {
897
+ const instance = this.instances.get(pluginSessionId);
898
+ if (!instance)
899
+ return [];
900
+ const events = [];
901
+ for (const request of this.pendingRequests.values()) {
902
+ if (request.targetInstanceId === instance.instanceId && request.targetRole === instance.role && this.requestStatuses.get(request.id)?.state === "started" && request.cancellationRequested) {
903
+ events.push(request.id);
904
+ }
905
+ }
906
+ return events;
907
+ }
908
+ async requestCancellation(requestId) {
909
+ const request = this.pendingRequests.get(requestId);
910
+ if (!request)
911
+ return false;
912
+ const status = this.requestStatuses.get(requestId);
913
+ if (status?.state === "started") {
914
+ request.cancellationRequested = true;
915
+ return true;
916
+ }
917
+ return this.cancelRequest(requestId);
918
+ }
919
+ isMutation(endpoint) {
920
+ return !endpoint.startsWith("/api/read") && !endpoint.startsWith("/api/get") && !endpoint.startsWith("/api/search") && endpoint !== "/api/evaluate" && endpoint !== "/api/execute-script" && endpoint !== "/api/run-code";
921
+ }
530
922
  };
531
923
  }
532
924
  });
@@ -593,7 +985,12 @@ function isRetryable(code) {
593
985
  }
594
986
  function toolErrorResult(error, stage) {
595
987
  const message = error instanceof Error ? error.message : String(error);
596
- const env = errorEnvelope(message, stage ? { stage } : {});
988
+ const unknownOutcome = error;
989
+ const busy = error;
990
+ const env = errorEnvelope(message, {
991
+ ...stage ? { stage } : {},
992
+ ...unknownOutcome?.outcome === "unknown" && typeof unknownOutcome.requestId === "string" ? { code: "OUTCOME_UNKNOWN", details: { requestId: unknownOutcome.requestId, outcome: "unknown" } } : typeof busy.retryAfterMs === "number" ? { code: "BUSY", details: { retryAfterMs: busy.retryAfterMs } } : {}
993
+ });
597
994
  return { content: [{ type: "text", text: JSON.stringify(env) }], isError: true };
598
995
  }
599
996
  function errorEnvelope(message, opts = {}) {
@@ -635,6 +1032,8 @@ var init_errors = __esm({
635
1032
  "use strict";
636
1033
  PATTERNS = [
637
1034
  // Order matters: more specific first.
1035
+ [/outcome\s+is\s+unknown|outcome_unknown/i, "OUTCOME_UNKNOWN"],
1036
+ [/bridge queue is busy|\bbusy\b/i, "BUSY"],
638
1037
  [/\b429\b|rate.?limit/i, "RATE_LIMITED"],
639
1038
  [/confirm(ation)?\s+required|requires?\s+confirm|pass\s+confirm/i, "CONFIRMATION_REQUIRED"],
640
1039
  [/multiple\s+(places|instances)|ambiguous\s+target|which\s+instance|specify\s+instance_id/i, "AMBIGUOUS_TARGET"],
@@ -648,11 +1047,14 @@ var init_errors = __esm({
648
1047
  [/required|missing\s+(argument|parameter|field)|must\s+be\s+(a|an)\b/i, "INVALID_ARGUMENT"]
649
1048
  ];
650
1049
  RETRYABLE = /* @__PURE__ */ new Set([
1050
+ "BUSY",
651
1051
  "TIMEOUT",
652
1052
  "RATE_LIMITED",
653
1053
  "PLUGIN_DISCONNECTED"
654
1054
  ]);
655
1055
  RECOVERY = {
1056
+ BUSY: "wait for retryAfterMs, then retry the request",
1057
+ OUTCOME_UNKNOWN: "call get_request_status with details.requestId before deciding whether to retry",
656
1058
  TIMEOUT: "retry; for heavy code prefer execute_luau_async",
657
1059
  RATE_LIMITED: "back off and retry shortly",
658
1060
  PLUGIN_DISCONNECTED: "check the Studio plugin is connected, then retry",
@@ -697,6 +1099,40 @@ var init_structured_output = __esm({
697
1099
  }
698
1100
  });
699
1101
 
1102
+ // ../core/dist/capability-policy.js
1103
+ function requiredCapability(toolName, category) {
1104
+ if (EXECUTE_TOOLS.test(toolName))
1105
+ return "execute.luau";
1106
+ if (ASSET_TOOLS.test(toolName))
1107
+ return "assets.external";
1108
+ if (PLAYTEST_TOOLS.test(toolName))
1109
+ return "playtest.control";
1110
+ if (PROPERTY_TOOLS.test(toolName))
1111
+ return "write.properties";
1112
+ return category === "write" ? "write.instances" : "read.scene";
1113
+ }
1114
+ function parseCapabilities(value) {
1115
+ if (!value?.trim())
1116
+ return void 0;
1117
+ return new Set(value.split(",").map((entry) => entry.trim()).filter(Boolean));
1118
+ }
1119
+ function parseClientCapabilities(value) {
1120
+ if (!value?.trim())
1121
+ return /* @__PURE__ */ new Map();
1122
+ const parsed = JSON.parse(value);
1123
+ return new Map(Object.entries(parsed).map(([token, capabilities]) => [token, new Set(capabilities)]));
1124
+ }
1125
+ var PROPERTY_TOOLS, EXECUTE_TOOLS, ASSET_TOOLS, PLAYTEST_TOOLS;
1126
+ var init_capability_policy = __esm({
1127
+ "../core/dist/capability-policy.js"() {
1128
+ "use strict";
1129
+ PROPERTY_TOOLS = /^(set_property|set_properties|mass_set_property|set_attribute|bulk_set_attributes|add_tag|remove_tag|delete_attribute)$/;
1130
+ EXECUTE_TOOLS = /^(execute_luau|execute_luau_async|eval_.*runtime|apply_mutation_plan)$/;
1131
+ ASSET_TOOLS = /(asset|wally|publish|import_rbxm|export_rbxm)/;
1132
+ PLAYTEST_TOOLS = /(playtest|multiplayer|simulate_(mouse|keyboard)|character_navigation)/;
1133
+ }
1134
+ });
1135
+
700
1136
  // ../core/dist/server-instructions.js
701
1137
  var SERVER_INSTRUCTIONS;
702
1138
  var init_server_instructions = __esm({
@@ -886,8 +1322,8 @@ function parseResourceUri(uri) {
886
1322
  return { kind: "changes", since };
887
1323
  }
888
1324
  if (segments[0] === "node" && segments.length >= 2) {
889
- const path7 = decodeURIComponent(segments.slice(1).join("/"));
890
- return { kind: "node", path: path7 };
1325
+ const path9 = decodeURIComponent(segments.slice(1).join("/"));
1326
+ return { kind: "node", path: path9 };
891
1327
  }
892
1328
  if (segments[0] === "playtest" && segments[1] === "episode" && segments.length >= 3) {
893
1329
  return { kind: "episode", id: decodeURIComponent(segments.slice(2).join("/")) };
@@ -1104,6 +1540,20 @@ var init_tool_catalog = __esm({
1104
1540
  "load_toolset",
1105
1541
  "get_roblox_docs",
1106
1542
  "get_session_summary",
1543
+ "get_request_status",
1544
+ "get_transport_diagnostics",
1545
+ "cancel_request",
1546
+ "detect_roblox_project",
1547
+ "validate_script_source",
1548
+ "format_script_preview",
1549
+ "resolve_instance_source_file",
1550
+ "run_project_tests",
1551
+ "get_dependency_graph",
1552
+ "install_wally_packages",
1553
+ "run_quality_gate",
1554
+ "validate_with_luau_lsp",
1555
+ "generate_rojo_sourcemap",
1556
+ "build_rojo_project",
1107
1557
  "get_connected_instances",
1108
1558
  "get_scene_summary",
1109
1559
  "execute_luau",
@@ -1209,7 +1659,6 @@ var init_tool_catalog = __esm({
1209
1659
 
1210
1660
  // ../core/dist/http-server.js
1211
1661
  import express from "express";
1212
- import cors from "cors";
1213
1662
  import http from "http";
1214
1663
  import { WebSocketServer, WebSocket } from "ws";
1215
1664
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -1217,11 +1666,29 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
1217
1666
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
1218
1667
  function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1219
1668
  const app = express();
1669
+ const requirePluginAuth = process.env.NODE_ENV !== "test";
1670
+ const bearerToken = (req) => {
1671
+ const value = req.header("authorization");
1672
+ return value?.startsWith("Bearer ") ? value.slice(7) : void 0;
1673
+ };
1674
+ const pluginAuthorized = (req, pluginSessionId) => !requirePluginAuth || !!pluginSessionId && bridge.authenticatePlugin(pluginSessionId, bearerToken(req) ?? "");
1675
+ const serverSessionToken = process.env.BLOXFORGE_SESSION_TOKEN?.trim();
1676
+ const clientCapabilities = parseClientCapabilities(process.env.BLOXFORGE_CLIENT_CAPABILITIES_JSON);
1677
+ app.use((req, res, next) => {
1678
+ const protectedServerRoute = req.path === "/proxy" || req.path === "/mcp" || req.path.startsWith("/mcp/");
1679
+ const token = bearerToken(req);
1680
+ if (protectedServerRoute && (serverSessionToken || clientCapabilities.size > 0) && token !== serverSessionToken && !clientCapabilities.has(token ?? "")) {
1681
+ res.status(401).json({ error: "invalid_session_token" });
1682
+ return;
1683
+ }
1684
+ next();
1685
+ });
1220
1686
  let mcpServerActive = false;
1221
1687
  let lastMCPActivity = 0;
1222
1688
  let mcpServerStartTime = 0;
1223
1689
  const proxyInstances = /* @__PURE__ */ new Set();
1224
1690
  const warnedVersionMismatches = /* @__PURE__ */ new Set();
1691
+ const warnedProtocolMismatches = /* @__PURE__ */ new Set();
1225
1692
  const setMCPServerActive = (active) => {
1226
1693
  mcpServerActive = active;
1227
1694
  if (active) {
@@ -1249,9 +1716,9 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1249
1716
  const isLazyTools = () => registry ? registry.lazyMode : !(envLazyFlag === "0" || envLazyFlag === "false" || envLazyFlag === "off");
1250
1717
  const getActiveToolCount = () => isLazyTools() ? registry ? registry.activeNames.size : CORE_TOOLS.size : serverConfig?.tools.length ?? 0;
1251
1718
  const getLoadedToolsets = () => isLazyTools() ? [process.env.BLOXFORGE_TOOL_PROFILE?.trim().toLowerCase() || "core"] : ["all"];
1252
- app.use(cors());
1253
- app.use(express.json({ limit: "50mb" }));
1254
- app.use(express.urlencoded({ limit: "50mb", extended: true }));
1719
+ const bodyLimit = process.env.MCP_HTTP_BODY_LIMIT?.trim() || "50mb";
1720
+ app.use(express.json({ limit: bodyLimit }));
1721
+ app.use(express.urlencoded({ limit: bodyLimit, extended: true }));
1255
1722
  app.get("/health", (req, res) => {
1256
1723
  const instances = bridge.getInstances();
1257
1724
  const publicInstances = instances.map(toPublic);
@@ -1346,20 +1813,33 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1346
1813
  warnedVersionMismatches.add(pluginSessionId);
1347
1814
  console.error(`[version-mismatch] Studio plugin v${registered.pluginVersion} (${registered.pluginVariant}) does not match MCP server v${registered.serverVersion} for ${registered.instanceId}/${registered.role}`);
1348
1815
  }
1816
+ if (registered?.protocolMismatch && !warnedProtocolMismatches.has(pluginSessionId)) {
1817
+ warnedProtocolMismatches.add(pluginSessionId);
1818
+ console.error(`[protocol-mismatch] Studio plugin protocol v${registered.pluginProtocolVersion} does not match MCP server protocol v${registered.serverProtocolVersion} for ${registered.instanceId}/${registered.role}. Run --auto-install-plugin to update the plugin.`);
1819
+ }
1349
1820
  res.json({
1350
1821
  success: true,
1351
1822
  assignedRole: result.assignedRole,
1352
1823
  instanceId: result.instanceId,
1824
+ sessionToken: result.sessionToken,
1825
+ serverEpoch: bridge.serverEpoch,
1353
1826
  serverVersion: serverConfig?.version,
1354
1827
  serverProtocolVersion: MCP_PROTOCOL_VERSION,
1355
1828
  versionMismatch: registered?.versionMismatch ?? false,
1356
- protocolMismatch: registered?.protocolMismatch ?? false
1829
+ protocolMismatch: registered?.protocolMismatch ?? false,
1830
+ capabilities: ["core", "scene", "mutation", "scripts", "runtime", "assets", "ui", "environment", "terrain", "build", "media", "sync", "safety"]
1357
1831
  });
1358
1832
  });
1359
1833
  app.post("/disconnect", (req, res) => {
1360
1834
  const { pluginSessionId } = req.body;
1835
+ if (!pluginAuthorized(req, pluginSessionId)) {
1836
+ res.status(401).json({ success: false, error: "invalid_session_token" });
1837
+ return;
1838
+ }
1361
1839
  if (pluginSessionId) {
1362
1840
  bridge.unregisterInstance(pluginSessionId, "plugin_request");
1841
+ warnedVersionMismatches.delete(pluginSessionId);
1842
+ warnedProtocolMismatches.delete(pluginSessionId);
1363
1843
  }
1364
1844
  res.json({ success: true });
1365
1845
  });
@@ -1372,6 +1852,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1372
1852
  instances: publicInstances,
1373
1853
  serverVersion: serverConfig?.version,
1374
1854
  protocolVersion: MCP_PROTOCOL_VERSION,
1855
+ serverEpoch: bridge.serverEpoch,
1375
1856
  versionMismatch: publicInstances.some((inst) => inst.versionMismatch),
1376
1857
  protocolMismatch: publicInstances.some((inst) => inst.protocolMismatch),
1377
1858
  lazyTools: isLazyTools(),
@@ -1427,6 +1908,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1427
1908
  });
1428
1909
  app.get("/poll", (req, res) => {
1429
1910
  const pluginSessionId = req.query.pluginSessionId;
1911
+ if (!pluginAuthorized(req, pluginSessionId)) {
1912
+ res.status(401).json({ error: "invalid_session_token", knownInstance: false, request: null });
1913
+ return;
1914
+ }
1430
1915
  if (pluginSessionId) {
1431
1916
  bridge.updateInstanceActivity(pluginSessionId);
1432
1917
  }
@@ -1468,11 +1953,14 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1468
1953
  });
1469
1954
  return;
1470
1955
  }
1471
- const pendingRequest = knownInstance && callerInstanceId && callerRole ? bridge.getPendingRequest(callerInstanceId, callerRole) : null;
1956
+ const pendingRequest = knownInstance && pluginSessionId ? bridge.getPendingRequestForSession(pluginSessionId) : null;
1472
1957
  if (pendingRequest) {
1473
1958
  res.json({
1474
1959
  request: pendingRequest.request,
1475
1960
  requestId: pendingRequest.requestId,
1961
+ serverEpoch: pendingRequest.serverEpoch,
1962
+ deliveryAttempt: pendingRequest.deliveryAttempt,
1963
+ leaseToken: pendingRequest.leaseToken,
1476
1964
  mcpConnected: true,
1477
1965
  pluginConnected: true,
1478
1966
  knownInstance,
@@ -1483,7 +1971,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1483
1971
  protocolVersion: callerPluginProtocolVersion,
1484
1972
  versionMismatch,
1485
1973
  protocolMismatch,
1486
- proxyInstanceCount: proxyInstances.size
1974
+ proxyInstanceCount: proxyInstances.size,
1975
+ cancellations: pluginSessionId ? bridge.getCancellationEvents(pluginSessionId) : []
1487
1976
  });
1488
1977
  } else {
1489
1978
  res.json({
@@ -1498,16 +1987,79 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1498
1987
  protocolVersion: callerPluginProtocolVersion,
1499
1988
  versionMismatch,
1500
1989
  protocolMismatch,
1501
- proxyInstanceCount: proxyInstances.size
1990
+ proxyInstanceCount: proxyInstances.size,
1991
+ cancellations: pluginSessionId ? bridge.getCancellationEvents(pluginSessionId) : []
1502
1992
  });
1503
1993
  }
1504
1994
  });
1995
+ app.post("/reconcile", (req, res) => {
1996
+ const pluginSessionId = typeof req.body?.pluginSessionId === "string" ? req.body.pluginSessionId : void 0;
1997
+ const serverEpoch = typeof req.body?.serverEpoch === "string" ? req.body.serverEpoch : void 0;
1998
+ const receipts = Array.isArray(req.body?.receipts) ? req.body.receipts : void 0;
1999
+ if (!pluginAuthorized(req, pluginSessionId)) {
2000
+ res.status(401).json({ error: "invalid_session_token" });
2001
+ return;
2002
+ }
2003
+ if (!pluginSessionId || !serverEpoch || !receipts) {
2004
+ res.status(400).json({ error: "missing_reconcile_fields" });
2005
+ return;
2006
+ }
2007
+ const reconciled = bridge.reconcilePluginReceipts(pluginSessionId, serverEpoch, receipts);
2008
+ res.json({ success: true, reconciled });
2009
+ });
2010
+ app.post("/ack", (req, res) => {
2011
+ const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : "";
2012
+ const pluginSessionId = typeof req.body?.pluginSessionId === "string" ? req.body.pluginSessionId : void 0;
2013
+ if (!pluginAuthorized(req, pluginSessionId)) {
2014
+ res.status(401).json({ error: "invalid_session_token", requestId });
2015
+ return;
2016
+ }
2017
+ const fence = {
2018
+ serverEpoch: req.body?.serverEpoch,
2019
+ pluginSessionId: pluginSessionId ?? "",
2020
+ deliveryAttempt: req.body?.deliveryAttempt,
2021
+ leaseToken: req.body?.leaseToken
2022
+ };
2023
+ const requiresFence = !!pluginSessionId && (bridge.getInstanceBySessionId(pluginSessionId)?.pluginProtocolVersion ?? 0) >= 3;
2024
+ if (!requestId || !bridge.getRequestStatus(requestId)) {
2025
+ res.status(404).json({ error: "not_found", requestId });
2026
+ return;
2027
+ }
2028
+ const acknowledged = requiresFence ? bridge.acknowledgeFencedRequest(requestId, fence) : bridge.acknowledgeRequest(requestId);
2029
+ if (!acknowledged) {
2030
+ res.status(409).json({ error: "stale_or_unknown_delivery", requestId });
2031
+ return;
2032
+ }
2033
+ res.json({ success: true, requestId });
2034
+ });
2035
+ app.post("/cancel", (req, res) => {
2036
+ const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : "";
2037
+ if (!requestId || !bridge.cancelRequest(requestId)) {
2038
+ res.status(409).json({ success: false, error: "request_not_cancellable", requestId });
2039
+ return;
2040
+ }
2041
+ res.json({ success: true, requestId, state: "cancelled" });
2042
+ });
2043
+ app.get("/request/:requestId/status", (req, res) => {
2044
+ const status = bridge.getRequestStatus(req.params.requestId);
2045
+ if (!status) {
2046
+ res.status(404).json({ error: "request_not_found", requestId: req.params.requestId });
2047
+ return;
2048
+ }
2049
+ res.json(status);
2050
+ });
1505
2051
  app.post("/response", (req, res) => {
1506
- const { requestId, response, error } = req.body;
1507
- if (error) {
1508
- bridge.rejectRequest(requestId, error);
1509
- } else {
1510
- bridge.resolveRequest(requestId, response);
2052
+ const { requestId, response, error, pluginSessionId, serverEpoch, deliveryAttempt, leaseToken } = req.body;
2053
+ if (!pluginAuthorized(req, pluginSessionId)) {
2054
+ res.status(401).json({ success: false, error: "invalid_session_token", requestId });
2055
+ return;
2056
+ }
2057
+ const fence = { serverEpoch, pluginSessionId, deliveryAttempt, leaseToken };
2058
+ const requiresFence = (bridge.getInstanceBySessionId(pluginSessionId)?.pluginProtocolVersion ?? 0) >= 3;
2059
+ const accepted = requiresFence ? error ? bridge.rejectFencedRequest(requestId, error, fence) : bridge.resolveFencedRequest(requestId, response, fence) : error ? (bridge.rejectRequest(requestId, error), true) : (bridge.resolveRequest(requestId, response), true);
2060
+ if (!accepted) {
2061
+ res.status(409).json({ success: false, error: "stale_or_unknown_delivery", requestId });
2062
+ return;
1511
2063
  }
1512
2064
  res.json({ success: true });
1513
2065
  });
@@ -1524,7 +2076,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1524
2076
  const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole);
1525
2077
  res.json({ response });
1526
2078
  } catch (err) {
1527
- res.status(500).json({ error: err.message || "Proxy request failed" });
2079
+ res.status(500).json({
2080
+ error: err.message || "Proxy request failed",
2081
+ ...err instanceof RequestOutcomeUnknownError ? { outcome: err.outcome, requestId: err.requestId } : {}
2082
+ });
1528
2083
  }
1529
2084
  });
1530
2085
  if (serverConfig) {
@@ -1555,6 +2110,12 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1555
2110
  if (allowedTools && !allowedTools.has(name)) {
1556
2111
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1557
2112
  }
2113
+ const definition = legacyFilteredTools.find((tool) => tool.name === name);
2114
+ const capabilities = clientCapabilities.get(bearerToken(req) ?? "");
2115
+ const capability = definition ? requiredCapability(name, definition.category) : void 0;
2116
+ if (capability && capabilities && !capabilities.has(capability)) {
2117
+ throw new McpError(ErrorCode.InvalidRequest, `Capability required: ${capability}`);
2118
+ }
1558
2119
  try {
1559
2120
  if (registry) {
1560
2121
  const registryResult = await registry.callTool(name, tools, args || {});
@@ -1686,12 +2247,12 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1686
2247
  return app;
1687
2248
  }
1688
2249
  function listenWithRetry(app, host, startPort, maxAttempts = 5) {
1689
- return new Promise(async (resolve6, reject) => {
2250
+ return new Promise(async (resolve7, reject) => {
1690
2251
  for (let i = 0; i < maxAttempts; i++) {
1691
2252
  const port = startPort + i;
1692
2253
  try {
1693
2254
  const server = await bindPort(app, host, port);
1694
- resolve6({ server, port });
2255
+ resolve7({ server, port });
1695
2256
  return;
1696
2257
  } catch (err) {
1697
2258
  if (err.code === "EADDRINUSE") {
@@ -1706,7 +2267,7 @@ function listenWithRetry(app, host, startPort, maxAttempts = 5) {
1706
2267
  });
1707
2268
  }
1708
2269
  function bindPort(app, host, port) {
1709
- return new Promise((resolve6, reject) => {
2270
+ return new Promise((resolve7, reject) => {
1710
2271
  const server = http.createServer(app);
1711
2272
  attachBridgeWebSocket(server, app.bridge);
1712
2273
  const onError = (err) => {
@@ -1716,11 +2277,12 @@ function bindPort(app, host, port) {
1716
2277
  server.once("error", onError);
1717
2278
  server.listen(port, host, () => {
1718
2279
  server.removeListener("error", onError);
1719
- resolve6(server);
2280
+ resolve7(server);
1720
2281
  });
1721
2282
  });
1722
2283
  }
1723
2284
  function attachBridgeWebSocket(server, bridge) {
2285
+ const requirePluginAuth = process.env.NODE_ENV !== "test";
1724
2286
  const streams = /* @__PURE__ */ new Map();
1725
2287
  const wss = new WebSocketServer({ noServer: true });
1726
2288
  const deliver = (pluginSessionId) => {
@@ -1741,7 +2303,8 @@ function attachBridgeWebSocket(server, bridge) {
1741
2303
  if (url.pathname !== "/stream")
1742
2304
  return;
1743
2305
  const pluginSessionId = url.searchParams.get("pluginSessionId");
1744
- if (!pluginSessionId || !bridge.getInstanceBySessionId(pluginSessionId)) {
2306
+ const sessionToken = url.searchParams.get("sessionToken") ?? void 0;
2307
+ if (!pluginSessionId || !bridge.getInstanceBySessionId(pluginSessionId) || requirePluginAuth && !bridge.authenticatePlugin(pluginSessionId, sessionToken ?? "")) {
1745
2308
  socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
1746
2309
  socket.destroy();
1747
2310
  return;
@@ -1756,9 +2319,30 @@ function attachBridgeWebSocket(server, bridge) {
1756
2319
  bridge.updateInstanceActivity(pluginSessionId);
1757
2320
  try {
1758
2321
  const message = JSON.parse(raw.toString());
1759
- if (message.type !== "response" || typeof message.requestId !== "string")
2322
+ if (typeof message.requestId !== "string")
2323
+ return;
2324
+ const fence = {
2325
+ serverEpoch: message.serverEpoch,
2326
+ pluginSessionId,
2327
+ deliveryAttempt: message.deliveryAttempt,
2328
+ leaseToken: message.leaseToken
2329
+ };
2330
+ const requiresFence = (bridge.getInstanceBySessionId(pluginSessionId)?.pluginProtocolVersion ?? 0) >= 3;
2331
+ if (message.type === "ack") {
2332
+ if (requiresFence)
2333
+ bridge.acknowledgeFencedRequest(message.requestId, fence);
2334
+ else
2335
+ bridge.acknowledgeRequest(message.requestId);
1760
2336
  return;
1761
- if (message.error)
2337
+ }
2338
+ if (message.type !== "response")
2339
+ return;
2340
+ if (requiresFence) {
2341
+ if (message.error)
2342
+ bridge.rejectFencedRequest(message.requestId, message.error, fence);
2343
+ else
2344
+ bridge.resolveFencedRequest(message.requestId, message.response, fence);
2345
+ } else if (message.error)
1762
2346
  bridge.rejectRequest(message.requestId, message.error);
1763
2347
  else
1764
2348
  bridge.resolveRequest(message.requestId, message.response);
@@ -1780,12 +2364,27 @@ var init_http_server = __esm({
1780
2364
  init_tool_shape();
1781
2365
  init_errors();
1782
2366
  init_structured_output();
2367
+ init_capability_policy();
1783
2368
  init_server_instructions();
1784
2369
  init_resources();
1785
2370
  init_tool_catalog();
1786
2371
  TOOL_HANDLERS = {
1787
2372
  get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
1788
2373
  get_session_summary: (tools) => tools.getSessionSummary(),
2374
+ get_request_status: (tools, body) => tools.getRequestStatus(body.requestId),
2375
+ get_transport_diagnostics: (tools) => tools.getTransportDiagnostics(),
2376
+ cancel_request: (tools, body) => tools.cancelRequest(body.requestId),
2377
+ detect_roblox_project: (tools, body) => tools.detectRobloxProject(body.root),
2378
+ validate_script_source: (tools, body) => tools.validateScriptSource(body.source, body.fileName),
2379
+ format_script_preview: (tools, body) => tools.formatScriptPreview(body.source, body.fileName),
2380
+ resolve_instance_source_file: (tools, body) => tools.resolveInstanceSourceFile(body.instancePath, body.root),
2381
+ run_project_tests: (tools, body) => tools.runProjectTests(body.root, body.script),
2382
+ get_dependency_graph: (tools, body) => tools.getDependencyGraph(body.root),
2383
+ install_wally_packages: (tools, body) => tools.installWallyPackages(body.root, body.confirm),
2384
+ run_quality_gate: (tools, body) => tools.runQualityGate(body.root),
2385
+ validate_with_luau_lsp: (tools, body) => tools.validateWithLuauLsp(body.root, body.files),
2386
+ generate_rojo_sourcemap: (tools, body) => tools.generateRojoSourcemap(body.root, body.output),
2387
+ build_rojo_project: (tools, body) => tools.buildRojoProject(body.root, body.output),
1789
2388
  tool_catalog_search: (tools, body) => tools.toolCatalogSearch(body),
1790
2389
  load_toolset: (tools, body) => tools.loadToolset(body),
1791
2390
  get_world_snapshot: (tools, body) => tools.getWorldSnapshot(body.path, body.level, body.topNPerClass, body.instance_id),
@@ -1793,7 +2392,7 @@ var init_http_server = __esm({
1793
2392
  get_changes_since: (tools, body) => tools.getChangesSince(body.snapshotId, body.path, body.instance_id),
1794
2393
  scene_search: (tools, body) => tools.sceneSearch(body.query, body.path, body.limit, body.instance_id),
1795
2394
  playtest_sample_state: (tools, body) => tools.playtestSampleState(body.domains, body.target, body.instance_id),
1796
- apply_mutation_plan: (tools, body) => tools.applyMutationPlan(body.operations, body.dryRun, body.confirm, body.instance_id),
2395
+ apply_mutation_plan: (tools, body) => tools.applyMutationPlan(body.operations, body.dryRun, body.confirm, body.instance_id, body.atomic),
1797
2396
  list_recipes: (tools) => tools.listRecipes(),
1798
2397
  apply_recipe: (tools, body) => tools.applyRecipe(body.recipe, body.params, body.instance_id),
1799
2398
  run_gameplay_assertions: (tools, body) => tools.runGameplayAssertions(body.assertions, body.target, body.instance_id),
@@ -2179,8 +2778,8 @@ var init_safety_manager = __esm({
2179
2778
  }
2180
2779
  return { allowed, requiresConfirmation, blocked, dryRun: false, reasons, warnings, matchedPatterns };
2181
2780
  }
2182
- isProtectedPath(path7) {
2183
- const trimmed = path7.trim();
2781
+ isProtectedPath(path9) {
2782
+ const trimmed = path9.trim();
2184
2783
  return this.config.protectedPaths.some((p) => p === trimmed || trimmed === `game.${p}`);
2185
2784
  }
2186
2785
  recordOperation(record) {
@@ -2193,10 +2792,10 @@ var init_safety_manager = __esm({
2193
2792
  getHistory() {
2194
2793
  return [...this.history].reverse();
2195
2794
  }
2196
- backupScript(path7, source) {
2197
- const existing = this.backups.get(path7);
2198
- this.backups.set(path7, {
2199
- path: path7,
2795
+ backupScript(path9, source) {
2796
+ const existing = this.backups.get(path9);
2797
+ this.backups.set(path9, {
2798
+ path: path9,
2200
2799
  source,
2201
2800
  previous: existing?.source,
2202
2801
  timestamp: Date.now()
@@ -2207,8 +2806,8 @@ var init_safety_manager = __esm({
2207
2806
  this.backups.delete(oldestKey);
2208
2807
  }
2209
2808
  }
2210
- getBackup(path7) {
2211
- return this.backups.get(path7);
2809
+ getBackup(path9) {
2810
+ return this.backups.get(path9);
2212
2811
  }
2213
2812
  listBackups() {
2214
2813
  return [...this.backups.values()].sort((a, b) => b.timestamp - a.timestamp);
@@ -2689,6 +3288,7 @@ local parent = resolvePath(${luaString(parent)})
2689
3288
  if not parent then return { error = "Parent not found: " .. ${luaString(parent)} } end
2690
3289
  local existing = parent:FindFirstChild(${luaString(name)})
2691
3290
  if existing then existing:Destroy() end
3291
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
2692
3292
  local door = Instance.new("Part")
2693
3293
  door.Name = ${luaString(name)}
2694
3294
  door.Size = Vector3.new(8, 10, 1)
@@ -2725,6 +3325,7 @@ local group = SoundService:FindFirstChild(groupName)
2725
3325
  if not group then group = Instance.new("SoundGroup") group.Name = groupName group.Parent = SoundService end
2726
3326
  local existing = SoundService:FindFirstChild(${luaString(name)})
2727
3327
  if existing then existing:Destroy() end
3328
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
2728
3329
  local sound = Instance.new("Sound")
2729
3330
  sound.Name = ${luaString(name)}
2730
3331
  sound.SoundId = ${luaString(soundId)}
@@ -2745,6 +3346,7 @@ local parent = resolvePath(${luaString(parent)})
2745
3346
  if not parent then return { error = "Parent not found: " .. ${luaString(parent)} } end
2746
3347
  local existing = parent:FindFirstChild(${luaString(name)})
2747
3348
  if existing then existing:Destroy() end
3349
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
2748
3350
  local brick = Instance.new("Part")
2749
3351
  brick.Name = ${luaString(name)}
2750
3352
  brick.Size = Vector3.new(8, 1, 8)
@@ -3396,12 +3998,14 @@ var init_mutation = __esm({
3396
3998
  property: { type: "string", description: "For set_property." },
3397
3999
  name: { type: "string", description: "For set_attribute." },
3398
4000
  tag: { type: "string", description: "For add_tag / remove_tag." },
3399
- value: { type: ["boolean", "number", "string"], description: "For set_property / set_attribute (primitive)." }
4001
+ value: { type: ["boolean", "number", "string"], description: "For set_property / set_attribute (primitive)." },
4002
+ expected: { type: ["boolean", "number", "string", "null"], description: "Optional optimistic-lock value. The whole plan is rejected before writes when the current value differs." }
3400
4003
  },
3401
4004
  required: ["op", "target"]
3402
4005
  }
3403
4006
  },
3404
4007
  dryRun: { type: "boolean", description: "Preview the diff without applying (default false)." },
4008
+ atomic: { type: "boolean", description: "Rollback earlier successful operations when a later operation fails (default true)." },
3405
4009
  confirm: { type: "boolean", description: "Approve a large plan the safety layer gates." },
3406
4010
  instance_id: { type: "string", description: "Connected Studio place id. Required only when multiple places are open." }
3407
4011
  },
@@ -3828,7 +4432,7 @@ var init_scripting = __esm({
3828
4432
  },
3829
4433
  usePattern: {
3830
4434
  type: "boolean",
3831
- description: "Use Lua pattern matching instead of literal (default: false)"
4435
+ description: "Use Lua pattern matching instead of literal (default: false). Lua patterns do not support regex alternation; | is literal, so run separate searches for alternatives."
3832
4436
  },
3833
4437
  contextLines: {
3834
4438
  type: "number",
@@ -6820,6 +7424,102 @@ var init_meta = __esm({
6820
7424
  properties: {}
6821
7425
  }
6822
7426
  },
7427
+ {
7428
+ name: "get_request_status",
7429
+ category: "read",
7430
+ description: "Look up a bridge request after a timeout. Use the requestId from an outcome_unknown error before deciding whether a mutation is safe to retry.",
7431
+ inputSchema: {
7432
+ type: "object",
7433
+ properties: {
7434
+ requestId: { type: "string", description: "Bridge request id from an outcome_unknown error." }
7435
+ },
7436
+ required: ["requestId"]
7437
+ }
7438
+ },
7439
+ {
7440
+ name: "get_transport_diagnostics",
7441
+ category: "read",
7442
+ description: "Return payload-free local transport metrics: queue depth, retries, BUSY/outcome_unknown/cancel/completion counts, server epoch, and latency p50/p95/p99.",
7443
+ inputSchema: { type: "object", properties: {} }
7444
+ },
7445
+ {
7446
+ name: "cancel_request",
7447
+ category: "write",
7448
+ description: "Cancel a queued bridge request before the plugin acknowledges execution. Returns false once execution has started; this never claims to stop an already-running mutation.",
7449
+ inputSchema: {
7450
+ type: "object",
7451
+ properties: {
7452
+ requestId: { type: "string", description: "Bridge request id to cancel." }
7453
+ },
7454
+ required: ["requestId"]
7455
+ }
7456
+ },
7457
+ {
7458
+ name: "detect_roblox_project",
7459
+ category: "read",
7460
+ description: "Detect Rojo, Wally, Rokit, Selene, StyLua, and sourcemap files and report which optional quality tools are installed.",
7461
+ inputSchema: { type: "object", properties: { root: { type: "string", description: "Project directory (defaults to the server working directory)." } } }
7462
+ },
7463
+ {
7464
+ name: "validate_script_source",
7465
+ category: "read",
7466
+ description: "Run available luau-analyze, Selene, and StyLua checks on source without writing it to the Roblox DataModel. Missing binaries are reported explicitly.",
7467
+ inputSchema: { type: "object", properties: { source: { type: "string" }, fileName: { type: "string" } }, required: ["source"] }
7468
+ },
7469
+ {
7470
+ name: "format_script_preview",
7471
+ category: "read",
7472
+ description: "Return a StyLua formatting preview through stdin; user source is never formatted or written implicitly.",
7473
+ inputSchema: { type: "object", properties: { source: { type: "string" }, fileName: { type: "string" } }, required: ["source"] }
7474
+ },
7475
+ {
7476
+ name: "resolve_instance_source_file",
7477
+ category: "read",
7478
+ description: "Resolve an Instance path through a Rojo-style sourcemap.json when one is present.",
7479
+ inputSchema: { type: "object", properties: { instancePath: { type: "string" }, root: { type: "string" } }, required: ["instancePath"] }
7480
+ },
7481
+ {
7482
+ name: "run_project_tests",
7483
+ category: "write",
7484
+ description: "Run an explicitly selected Lune project test script; no test script is guessed.",
7485
+ inputSchema: { type: "object", properties: { root: { type: "string" }, script: { type: "string" } }, required: ["script"] }
7486
+ },
7487
+ {
7488
+ name: "get_dependency_graph",
7489
+ category: "read",
7490
+ description: "Read Wally manifest/lockfile metadata into a compact dependency list without installing packages.",
7491
+ inputSchema: { type: "object", properties: { root: { type: "string" } } }
7492
+ },
7493
+ {
7494
+ name: "install_wally_packages",
7495
+ category: "write",
7496
+ description: "Run wally install in the detected project only after explicit confirm=true.",
7497
+ inputSchema: { type: "object", properties: { root: { type: "string" }, confirm: { type: "boolean" } }, required: ["confirm"] }
7498
+ },
7499
+ {
7500
+ name: "run_quality_gate",
7501
+ category: "read",
7502
+ description: "Run available Rojo sourcemap, Selene, StyLua, and luau-lsp project checks and return structured per-tool results.",
7503
+ inputSchema: { type: "object", properties: { root: { type: "string" } } }
7504
+ },
7505
+ {
7506
+ name: "validate_with_luau_lsp",
7507
+ category: "read",
7508
+ description: "Run luau-lsp analyze against selected files, automatically using the detected Rojo sourcemap when present.",
7509
+ inputSchema: { type: "object", properties: { root: { type: "string" }, files: { type: "array", items: { type: "string" } } } }
7510
+ },
7511
+ {
7512
+ name: "generate_rojo_sourcemap",
7513
+ category: "write",
7514
+ description: "Generate a Rojo sourcemap from the detected project file into an explicit output path.",
7515
+ inputSchema: { type: "object", properties: { root: { type: "string" }, output: { type: "string" } } }
7516
+ },
7517
+ {
7518
+ name: "build_rojo_project",
7519
+ category: "write",
7520
+ description: "Build the detected Rojo project to an explicit RBXL/RBXM output path.",
7521
+ inputSchema: { type: "object", properties: { root: { type: "string" }, output: { type: "string" } }, required: ["output"] }
7522
+ },
6823
7523
  {
6824
7524
  name: "load_toolset",
6825
7525
  category: "read",
@@ -7349,8 +8049,8 @@ for _, p in ipairs(paths) do
7349
8049
  end
7350
8050
  return { nodes = out, count = #out }`;
7351
8051
  }
7352
- function buildWorldSnapshotLuau(path7 = "game", level = "overview", topNPerClass = 12) {
7353
- const safePath = luaString(path7);
8052
+ function buildWorldSnapshotLuau(path9 = "game", level = "overview", topNPerClass = 12) {
8053
+ const safePath = luaString(path9);
7354
8054
  const safeTopN = luaNumber(Math.max(1, Math.floor(topNPerClass)));
7355
8055
  return `${PATH_RESOLVER_LUA}
7356
8056
  local root = resolvePath(${safePath})
@@ -7362,6 +8062,8 @@ local soundCount, soundPlaying, soundLooped = 0, 0, 0
7362
8062
  local scriptCount, localScriptCount, moduleCount = 0, 0, 0
7363
8063
  local taggedCount = 0
7364
8064
  for _, d in ipairs(root:GetDescendants()) do
8065
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
8066
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
7365
8067
  total = total + 1
7366
8068
  byClass[d.ClassName] = (byClass[d.ClassName] or 0) + 1
7367
8069
  if d:IsA("Sound") then
@@ -7458,8 +8160,8 @@ end`;
7458
8160
  });
7459
8161
 
7460
8162
  // ../core/dist/builders/scene-search.js
7461
- function buildSceneSearchLuau(query, path7 = "game", limit = 10) {
7462
- const safePath = luaString(path7);
8163
+ function buildSceneSearchLuau(query, path9 = "game", limit = 10) {
8164
+ const safePath = luaString(path9);
7463
8165
  const safeQuery = luaString(query.toLowerCase());
7464
8166
  const safeLimit = luaNumber(Math.max(1, Math.min(50, Math.floor(limit))));
7465
8167
  return `${PATH_RESOLVER_LUA}
@@ -7479,6 +8181,8 @@ end
7479
8181
 
7480
8182
  local scored = {}
7481
8183
  for _, d in ipairs(root:GetDescendants()) do
8184
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
8185
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
7482
8186
  local name = lc(d.Name)
7483
8187
  local class = lc(d.ClassName)
7484
8188
  local parentName = d.Parent and lc(d.Parent.Name) or ""
@@ -7525,8 +8229,8 @@ var init_scene_search = __esm({
7525
8229
  });
7526
8230
 
7527
8231
  // ../core/dist/builders/world-fingerprint.js
7528
- function buildWorldFingerprintLuau(path7 = "game", maxNodes = 8e3) {
7529
- const safePath = luaString(path7);
8232
+ function buildWorldFingerprintLuau(path9 = "game", maxNodes = 8e3) {
8233
+ const safePath = luaString(path9);
7530
8234
  const safeMax = luaNumber(Math.max(1, Math.floor(maxNodes)));
7531
8235
  return `${PATH_RESOLVER_LUA}
7532
8236
  ${FINGERPRINT_HELPERS_LUA}
@@ -7536,6 +8240,8 @@ local fp = {}
7536
8240
  local count = 0
7537
8241
  local truncated = false
7538
8242
  for _, d in ipairs(root:GetDescendants()) do
8243
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
8244
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
7539
8245
  if count >= ${safeMax} then truncated = true break end
7540
8246
  local id = nid(d)
7541
8247
  fp[id] = {
@@ -7701,9 +8407,9 @@ var init_world_changes = __esm({
7701
8407
  this.seq += 1;
7702
8408
  return `snap_${Date.now().toString(36)}_${this.seq}`;
7703
8409
  }
7704
- put(path7, fingerprint) {
8410
+ put(path9, fingerprint) {
7705
8411
  const id = this.newId();
7706
- this.snapshots.set(id, { id, path: path7, fingerprint, createdAt: Date.now() });
8412
+ this.snapshots.set(id, { id, path: path9, fingerprint, createdAt: Date.now() });
7707
8413
  this.prune();
7708
8414
  return id;
7709
8415
  }
@@ -8763,7 +9469,7 @@ function encodeImageFromRgbaResponse(response, format, quality) {
8763
9469
  };
8764
9470
  }
8765
9471
  function sleep(ms) {
8766
- return new Promise((resolve6) => setTimeout(resolve6, ms));
9472
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
8767
9473
  }
8768
9474
  function errorMessage(error) {
8769
9475
  return error instanceof Error ? error.message : String(error);
@@ -9199,18 +9905,18 @@ var init_world_model_tools = __esm({
9199
9905
  constructor(runtime) {
9200
9906
  this.runtime = runtime;
9201
9907
  }
9202
- async getWorldSnapshot(path7, level, topNPerClass, instance_id) {
9203
- const code = buildWorldSnapshotLuau(path7 ?? "game", level ?? "overview", topNPerClass ?? 12);
9908
+ async getWorldSnapshot(path9, level, topNPerClass, instance_id) {
9909
+ const code = buildWorldSnapshotLuau(path9 ?? "game", level ?? "overview", topNPerClass ?? 12);
9204
9910
  const response = await this.runtime.callSingle("/api/execute-luau", { code }, "edit", instance_id);
9205
9911
  return wrapToolJsonText(normalizeExecuteLuauToolResult(response, {
9206
9912
  error: "get_world_snapshot returned non-object execute-luau output"
9207
9913
  }));
9208
9914
  }
9209
- async sceneSearch(query, path7, limit, instance_id) {
9915
+ async sceneSearch(query, path9, limit, instance_id) {
9210
9916
  if (!query || !query.trim()) {
9211
9917
  throw new Error("query is required for scene_search");
9212
9918
  }
9213
- const response = await this.runtime.callSingle("/api/execute-luau", { code: buildSceneSearchLuau(query, path7 ?? "game", limit ?? 10) }, "edit", instance_id);
9919
+ const response = await this.runtime.callSingle("/api/execute-luau", { code: buildSceneSearchLuau(query, path9 ?? "game", limit ?? 10) }, "edit", instance_id);
9214
9920
  return wrapToolJsonText(normalizeExecuteLuauToolResult(response, {
9215
9921
  query,
9216
9922
  total: 0,
@@ -9230,8 +9936,8 @@ var init_world_model_tools = __esm({
9230
9936
  error: "get_node_batch returned non-object execute-luau output"
9231
9937
  }));
9232
9938
  }
9233
- async _captureFingerprint(path7, instance_id) {
9234
- const response = await this.runtime.callSingle("/api/execute-luau", { code: buildWorldFingerprintLuau(path7) }, "edit", instance_id);
9939
+ async _captureFingerprint(path9, instance_id) {
9940
+ const response = await this.runtime.callSingle("/api/execute-luau", { code: buildWorldFingerprintLuau(path9) }, "edit", instance_id);
9235
9941
  try {
9236
9942
  const rv = response?.returnValue;
9237
9943
  if (typeof rv === "string") {
@@ -9244,8 +9950,8 @@ var init_world_model_tools = __esm({
9244
9950
  }
9245
9951
  return { fp: {}, count: 0, truncated: false, error: "Could not parse world fingerprint" };
9246
9952
  }
9247
- async getChangesSince(snapshotId, path7, instance_id) {
9248
- const p = path7 ?? "game";
9953
+ async getChangesSince(snapshotId, path9, instance_id) {
9954
+ const p = path9 ?? "game";
9249
9955
  const cur = await this._captureFingerprint(p, instance_id);
9250
9956
  const wrap4 = (obj) => ({ content: [{ type: "text", text: JSON.stringify(obj) }] });
9251
9957
  if (cur.error)
@@ -9409,8 +10115,8 @@ var init_response_shape = __esm({
9409
10115
  });
9410
10116
 
9411
10117
  // ../core/dist/builders/scene-summary.js
9412
- function buildSceneSummaryLuau(path7 = "game.Workspace", topN = 20) {
9413
- const safePath = luaString(path7);
10118
+ function buildSceneSummaryLuau(path9 = "game.Workspace", topN = 20) {
10119
+ const safePath = luaString(path9);
9414
10120
  const safeTopN = luaNumber(Math.max(1, Math.floor(topN)));
9415
10121
  return `${PATH_RESOLVER_LUA}
9416
10122
  local root = resolvePath(${safePath})
@@ -9459,8 +10165,8 @@ var init_scene_read_tools = __esm({
9459
10165
  constructor(runtime) {
9460
10166
  this.runtime = runtime;
9461
10167
  }
9462
- async getFileTree(path7 = "", instance_id) {
9463
- const response = await this.runtime.callSingle("/api/file-tree", { path: path7 }, void 0, instance_id);
10168
+ async getFileTree(path9 = "", instance_id) {
10169
+ const response = await this.runtime.callSingle("/api/file-tree", { path: path9 }, void 0, instance_id);
9464
10170
  return compactText(response);
9465
10171
  }
9466
10172
  async getPlaceInfo(instance_id) {
@@ -9514,9 +10220,9 @@ var init_scene_read_tools = __esm({
9514
10220
  const response = await this.runtime.callSingle("/api/class-info", { className }, void 0, instance_id);
9515
10221
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
9516
10222
  }
9517
- async getProjectStructure(path7, maxDepth, scriptsOnly, instance_id) {
10223
+ async getProjectStructure(path9, maxDepth, scriptsOnly, instance_id) {
9518
10224
  const response = await this.runtime.callSingle("/api/project-structure", {
9519
- path: path7,
10225
+ path: path9,
9520
10226
  maxDepth,
9521
10227
  scriptsOnly
9522
10228
  }, void 0, instance_id);
@@ -9812,13 +10518,14 @@ ${JSON.stringify({ errors: result.errors, warnings: result.warnings })}`
9812
10518
  });
9813
10519
 
9814
10520
  // ../core/dist/builders/mutation-plan.js
9815
- function buildMutationPlanLuau(operations, dryRun) {
10521
+ function buildMutationPlanLuau(operations, dryRun, atomic = true) {
9816
10522
  const opsJson = JSON.stringify(JSON.stringify(operations));
9817
10523
  return `${PATH_RESOLVER_LUA}
9818
10524
  local HttpService = game:GetService("HttpService")
9819
10525
  local CollectionService = game:GetService("CollectionService")
9820
10526
  local ops = HttpService:JSONDecode(${opsJson})
9821
10527
  local dryRun = ${luaBool(dryRun)}
10528
+ local atomic = ${luaBool(atomic)}
9822
10529
 
9823
10530
  local function ser(v)
9824
10531
  local t = typeof(v)
@@ -9829,8 +10536,26 @@ end
9829
10536
  local results = {}
9830
10537
  local rollback = {}
9831
10538
  local succeeded, failed = 0, 0
10539
+ local conflicts = {}
10540
+
10541
+ for index, op in ipairs(ops) do
10542
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { applied = false, cancelled = true } end
10543
+ if op.expected ~= nil then
10544
+ local inst = resolvePath(op.target)
10545
+ local current
10546
+ if inst and op.op == "set_property" then current = ser(inst[op.property])
10547
+ elseif inst and op.op == "set_attribute" then current = ser(inst:GetAttribute(op.name))
10548
+ elseif inst and (op.op == "add_tag" or op.op == "remove_tag") then current = CollectionService:HasTag(inst, op.tag) end
10549
+ if current ~= op.expected then table.insert(conflicts, { index = index, target = op.target, expected = op.expected, actual = current }) end
10550
+ end
10551
+ end
10552
+
10553
+ if #conflicts > 0 then
10554
+ return { applied = false, conflict = true, conflicts = conflicts, results = {}, rollback = {}, summary = { total = #ops, succeeded = 0, failed = 0 } }
10555
+ end
9832
10556
 
9833
10557
  for _, op in ipairs(ops) do
10558
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { applied = false, cancelled = true, rollback = rollback } end
9834
10559
  local r = { op = op.op, target = op.target }
9835
10560
  local inst = resolvePath(op.target)
9836
10561
  if not inst then
@@ -9887,9 +10612,26 @@ for _, op in ipairs(ops) do
9887
10612
  table.insert(results, r)
9888
10613
  end
9889
10614
 
10615
+ local rolledBack = false
10616
+ if atomic and not dryRun and failed > 0 then
10617
+ rolledBack = true
10618
+ for i = #rollback, 1, -1 do
10619
+ local op = rollback[i]
10620
+ local inst = resolvePath(op.target)
10621
+ if inst then pcall(function()
10622
+ if op.op == "set_property" then inst[op.property] = op.value
10623
+ elseif op.op == "set_attribute" then inst:SetAttribute(op.name, op.value)
10624
+ elseif op.op == "add_tag" then CollectionService:AddTag(inst, op.tag)
10625
+ elseif op.op == "remove_tag" then CollectionService:RemoveTag(inst, op.tag) end
10626
+ end) end
10627
+ end
10628
+ end
10629
+
9890
10630
  return {
9891
- applied = not dryRun,
10631
+ applied = not dryRun and not rolledBack,
9892
10632
  dryRun = dryRun,
10633
+ atomic = atomic,
10634
+ rolledBack = rolledBack,
9893
10635
  results = results,
9894
10636
  rollback = rollback,
9895
10637
  summary = { total = #ops, succeeded = succeeded, failed = failed },
@@ -10051,7 +10793,7 @@ var init_mutation_tools = __esm({
10051
10793
  }
10052
10794
  // Transactional batch mutations: apply many small edits in one round-trip with a
10053
10795
  // dry-run diff and a ready-to-run reverse plan in the receipt (stateless rollback).
10054
- async applyMutationPlan(operations, dryRun, confirm, instance_id) {
10796
+ async applyMutationPlan(operations, dryRun, confirm, instance_id, atomic = true) {
10055
10797
  if (!Array.isArray(operations) || operations.length === 0) {
10056
10798
  throw new Error("operations (a non-empty array) is required for apply_mutation_plan");
10057
10799
  }
@@ -10060,7 +10802,7 @@ var init_mutation_tools = __esm({
10060
10802
  if (gated)
10061
10803
  return gated;
10062
10804
  }
10063
- const response = await this.runtime.callSingle("/api/execute-luau", { code: buildMutationPlanLuau(operations, !!dryRun) }, "edit", instance_id);
10805
+ const response = await this.runtime.callSingle("/api/execute-luau", { code: buildMutationPlanLuau(operations, !!dryRun, atomic) }, "edit", instance_id);
10064
10806
  if (!dryRun)
10065
10807
  this.runtime.recordOperation("bulk_mutate", `mutation plan: ${operations.length} ops`);
10066
10808
  return wrapToolJsonText(normalizeExecuteLuauToolResult(response, {
@@ -10570,9 +11312,9 @@ var init_image_client = __esm({
10570
11312
  });
10571
11313
 
10572
11314
  // ../core/dist/tools/asset-tools.js
10573
- import * as fs from "fs";
10574
- import * as os from "os";
10575
- import * as path from "path";
11315
+ import * as fs2 from "fs";
11316
+ import * as os2 from "os";
11317
+ import * as path2 from "path";
10576
11318
  var AssetTools;
10577
11319
  var init_asset_tools = __esm({
10578
11320
  "../core/dist/tools/asset-tools.js"() {
@@ -10588,13 +11330,13 @@ var init_asset_tools = __esm({
10588
11330
  }
10589
11331
  // ─── Static helpers (library path) ────────────────────────────────
10590
11332
  static findProjectRoot(startDir) {
10591
- let dir = path.resolve(startDir);
11333
+ let dir = path2.resolve(startDir);
10592
11334
  let previous = "";
10593
11335
  while (dir !== previous) {
10594
- if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "package.json")))
11336
+ if (fs2.existsSync(path2.join(dir, ".git")) || fs2.existsSync(path2.join(dir, "package.json")))
10595
11337
  return dir;
10596
11338
  previous = dir;
10597
- dir = path.dirname(dir);
11339
+ dir = path2.dirname(dir);
10598
11340
  }
10599
11341
  return null;
10600
11342
  }
@@ -10602,22 +11344,22 @@ var init_asset_tools = __esm({
10602
11344
  if (!candidate)
10603
11345
  return false;
10604
11346
  try {
10605
- return fs.statSync(candidate).isDirectory();
11347
+ return fs2.statSync(candidate).isDirectory();
10606
11348
  } catch {
10607
11349
  return false;
10608
11350
  }
10609
11351
  }
10610
11352
  static ensureWritableDirectory(candidate, label) {
10611
- const resolved = path.resolve(candidate);
11353
+ const resolved = path2.resolve(candidate);
10612
11354
  try {
10613
- fs.mkdirSync(resolved, { recursive: true });
11355
+ fs2.mkdirSync(resolved, { recursive: true });
10614
11356
  } catch (error) {
10615
11357
  throw new Error(`Unable to create ${label} build-library directory at ${resolved}: ${error.message}`);
10616
11358
  }
10617
11359
  if (!_AssetTools.isDirectory(resolved))
10618
11360
  throw new Error(`${label} build-library path is not a directory: ${resolved}`);
10619
11361
  try {
10620
- fs.accessSync(resolved, fs.constants.W_OK);
11362
+ fs2.accessSync(resolved, fs2.constants.W_OK);
10621
11363
  } catch (error) {
10622
11364
  throw new Error(`${label} build-library directory is not writable: ${resolved}. ${error.message}`);
10623
11365
  }
@@ -10627,26 +11369,28 @@ var init_asset_tools = __esm({
10627
11369
  static findLibraryPath() {
10628
11370
  if (_AssetTools._cachedLibraryPath)
10629
11371
  return _AssetTools._cachedLibraryPath;
10630
- const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
10631
- const cwd = path.resolve(process.cwd());
11372
+ const overridePath = process.env.BLOXFORGE_BUILD_LIBRARY || process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
11373
+ const cwd = path2.resolve(process.cwd());
10632
11374
  const projectRoot = _AssetTools.findProjectRoot(cwd);
10633
- const homeLibraryPath = path.join(os.homedir(), ".robloxstudio-mcp", "build-library");
10634
- const projectLibraryPath = projectRoot ? path.join(projectRoot, "build-library") : null;
10635
- const cwdLibraryPath = path.join(cwd, "build-library");
11375
+ const newHomeLibraryPath = path2.join(os2.homedir(), ".bloxforge", "build-library");
11376
+ const legacyHomeLibraryPath = path2.join(os2.homedir(), ".robloxstudio-mcp", "build-library");
11377
+ const homeLibraryPath = fs2.existsSync(newHomeLibraryPath) ? newHomeLibraryPath : fs2.existsSync(legacyHomeLibraryPath) ? legacyHomeLibraryPath : newHomeLibraryPath;
11378
+ const projectLibraryPath = projectRoot ? path2.join(projectRoot, "build-library") : null;
11379
+ const cwdLibraryPath = path2.join(cwd, "build-library");
10636
11380
  let result;
10637
11381
  if (overridePath) {
10638
11382
  result = _AssetTools.ensureWritableDirectory(overridePath, "override");
10639
11383
  } else {
10640
11384
  const existing = [projectLibraryPath, cwdLibraryPath].find((c) => c && _AssetTools.isDirectory(c) && (() => {
10641
11385
  try {
10642
- fs.accessSync(c, fs.constants.W_OK);
11386
+ fs2.accessSync(c, fs2.constants.W_OK);
10643
11387
  return true;
10644
11388
  } catch {
10645
11389
  return false;
10646
11390
  }
10647
11391
  })());
10648
11392
  if (existing) {
10649
- result = path.resolve(existing);
11393
+ result = path2.resolve(existing);
10650
11394
  } else if (projectLibraryPath) {
10651
11395
  try {
10652
11396
  result = _AssetTools.ensureWritableDirectory(projectLibraryPath, "project-root");
@@ -10767,11 +11511,11 @@ var init_asset_tools = __esm({
10767
11511
  if (response && response.success && response.buildData) {
10768
11512
  const buildData = response.buildData;
10769
11513
  const buildId = buildData.id || `${style}/exported`;
10770
- const filePath = path.join(_AssetTools.findLibraryPath(), `${buildId}.json`);
10771
- const dirPath = path.dirname(filePath);
10772
- if (!fs.existsSync(dirPath))
10773
- fs.mkdirSync(dirPath, { recursive: true });
10774
- fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
11514
+ const filePath = path2.join(_AssetTools.findLibraryPath(), `${buildId}.json`);
11515
+ const dirPath = path2.dirname(filePath);
11516
+ if (!fs2.existsSync(dirPath))
11517
+ fs2.mkdirSync(dirPath, { recursive: true });
11518
+ fs2.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
10775
11519
  response.savedTo = filePath;
10776
11520
  }
10777
11521
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
@@ -10783,11 +11527,11 @@ var init_asset_tools = __esm({
10783
11527
  const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
10784
11528
  const computedBounds = bounds || this.computeBounds(normalizedParts);
10785
11529
  const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
10786
- const filePath = path.join(_AssetTools.findLibraryPath(), `${id}.json`);
10787
- const dirPath = path.dirname(filePath);
10788
- if (!fs.existsSync(dirPath))
10789
- fs.mkdirSync(dirPath, { recursive: true });
10790
- fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
11530
+ const filePath = path2.join(_AssetTools.findLibraryPath(), `${id}.json`);
11531
+ const dirPath = path2.dirname(filePath);
11532
+ if (!fs2.existsSync(dirPath))
11533
+ fs2.mkdirSync(dirPath, { recursive: true });
11534
+ fs2.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
10791
11535
  return {
10792
11536
  content: [{ type: "text", text: JSON.stringify({
10793
11537
  success: true,
@@ -10812,11 +11556,11 @@ var init_asset_tools = __esm({
10812
11556
  const buildData = { id, style, bounds: result.bounds, palette, parts: result.parts, generatorCode: code };
10813
11557
  if (seed !== void 0)
10814
11558
  buildData.generatorSeed = seed;
10815
- const filePath = path.join(_AssetTools.findLibraryPath(), `${id}.json`);
10816
- const dirPath = path.dirname(filePath);
10817
- if (!fs.existsSync(dirPath))
10818
- fs.mkdirSync(dirPath, { recursive: true });
10819
- fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
11559
+ const filePath = path2.join(_AssetTools.findLibraryPath(), `${id}.json`);
11560
+ const dirPath = path2.dirname(filePath);
11561
+ if (!fs2.existsSync(dirPath))
11562
+ fs2.mkdirSync(dirPath, { recursive: true });
11563
+ fs2.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
10820
11564
  return {
10821
11565
  content: [{ type: "text", text: JSON.stringify({
10822
11566
  success: true,
@@ -10830,11 +11574,11 @@ var init_asset_tools = __esm({
10830
11574
  }
10831
11575
  async importBuild(buildData, targetPath, position, instance_id) {
10832
11576
  if (typeof buildData === "string") {
10833
- const filePath = path.join(_AssetTools.findLibraryPath(), `${buildData}.json`);
10834
- if (!fs.existsSync(filePath)) {
11577
+ const filePath = path2.join(_AssetTools.findLibraryPath(), `${buildData}.json`);
11578
+ if (!fs2.existsSync(filePath)) {
10835
11579
  return toolErrorResult(`Build not found in library: ${buildData}`);
10836
11580
  }
10837
- buildData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
11581
+ buildData = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
10838
11582
  }
10839
11583
  const response = await this.runtime.callSingle("/api/import-build", {
10840
11584
  buildData,
@@ -10846,14 +11590,14 @@ var init_asset_tools = __esm({
10846
11590
  async listLibrary(style) {
10847
11591
  const libPath = _AssetTools.findLibraryPath();
10848
11592
  let entries = [];
10849
- if (fs.existsSync(libPath)) {
11593
+ if (fs2.existsSync(libPath)) {
10850
11594
  const readDir = (dir) => {
10851
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
11595
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
10852
11596
  if (entry.isDirectory()) {
10853
- readDir(path.join(dir, entry.name));
11597
+ readDir(path2.join(dir, entry.name));
10854
11598
  } else if (entry.name.endsWith(".json")) {
10855
11599
  try {
10856
- const data = JSON.parse(fs.readFileSync(path.join(dir, entry.name), "utf-8"));
11600
+ const data = JSON.parse(fs2.readFileSync(path2.join(dir, entry.name), "utf-8"));
10857
11601
  if (style && data.style !== style)
10858
11602
  continue;
10859
11603
  entries.push({
@@ -10878,10 +11622,10 @@ var init_asset_tools = __esm({
10878
11622
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
10879
11623
  }
10880
11624
  async getBuild(id) {
10881
- const filePath = path.join(_AssetTools.findLibraryPath(), `${id}.json`);
10882
- if (!fs.existsSync(filePath))
11625
+ const filePath = path2.join(_AssetTools.findLibraryPath(), `${id}.json`);
11626
+ if (!fs2.existsSync(filePath))
10883
11627
  return toolErrorResult(`Build not found in library: ${id}`);
10884
- const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
11628
+ const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
10885
11629
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
10886
11630
  }
10887
11631
  async importScene(sceneData, targetPath, instance_id) {
@@ -10925,10 +11669,10 @@ var init_asset_tools = __esm({
10925
11669
  const buildId = models[modelKey];
10926
11670
  if (!buildId)
10927
11671
  throw new Error(`Unknown model key "${modelKey}" in scene placement`);
10928
- const filePath = path.join(_AssetTools.findLibraryPath(), `${buildId}.json`);
10929
- if (!fs.existsSync(filePath))
11672
+ const filePath = path2.join(_AssetTools.findLibraryPath(), `${buildId}.json`);
11673
+ if (!fs2.existsSync(filePath))
10930
11674
  throw new Error(`Build "${buildId}" not found in library`);
10931
- const buildData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
11675
+ const buildData = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
10932
11676
  await this.runtime.callSingle("/api/import-build", {
10933
11677
  buildData,
10934
11678
  targetPath: targetPath || "Workspace",
@@ -11094,10 +11838,10 @@ var init_asset_tools = __esm({
11094
11838
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
11095
11839
  }
11096
11840
  async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
11097
- if (!fs.existsSync(filePath))
11841
+ if (!fs2.existsSync(filePath))
11098
11842
  throw new Error(`File not found: ${filePath}`);
11099
- const fileContent = fs.readFileSync(filePath);
11100
- const fileName = path.basename(filePath);
11843
+ const fileContent = fs2.readFileSync(filePath);
11844
+ const fileName = path2.basename(filePath);
11101
11845
  const cc = this.runtime.cookieClient;
11102
11846
  const oc = this.runtime.openCloudClient;
11103
11847
  if (assetType === "Decal" && cc.hasCookie()) {
@@ -11146,10 +11890,10 @@ var init_asset_tools = __esm({
11146
11890
  if (!response.base64)
11147
11891
  return toolErrorResult("plugin returned no base64 payload");
11148
11892
  const bytes = Buffer.from(response.base64, "base64");
11149
- const resolved = path.resolve(outputPath);
11893
+ const resolved = path2.resolve(outputPath);
11150
11894
  try {
11151
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
11152
- fs.writeFileSync(resolved, bytes);
11895
+ fs2.mkdirSync(path2.dirname(resolved), { recursive: true });
11896
+ fs2.writeFileSync(resolved, bytes);
11153
11897
  } catch (err) {
11154
11898
  return toolErrorResult(`failed to write ${resolved}: ${err.message}`);
11155
11899
  }
@@ -11173,9 +11917,9 @@ var init_asset_tools = __esm({
11173
11917
  let bytes;
11174
11918
  let sourceLabel;
11175
11919
  if (source.path !== void 0) {
11176
- const resolved = path.resolve(source.path);
11920
+ const resolved = path2.resolve(source.path);
11177
11921
  try {
11178
- bytes = fs.readFileSync(resolved);
11922
+ bytes = fs2.readFileSync(resolved);
11179
11923
  } catch (err) {
11180
11924
  return toolErrorResult(`failed to read ${resolved}: ${err.message}`);
11181
11925
  }
@@ -11227,10 +11971,10 @@ var init_asset_tools = __esm({
11227
11971
  const { buffer, contentType } = await this.runtime.imageClient.generate(prompt, options ?? {});
11228
11972
  const ext = contentType.includes("png") ? "png" : "jpg";
11229
11973
  const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "image";
11230
- const dir = path.resolve(process.env.ROBLOX_IMAGE_DIR ?? path.join(process.cwd(), "generated-images"));
11231
- fs.mkdirSync(dir, { recursive: true });
11232
- const file = path.join(dir, `${slug}-${Date.now()}.${ext}`);
11233
- fs.writeFileSync(file, buffer);
11974
+ const dir = path2.resolve(process.env.ROBLOX_IMAGE_DIR ?? path2.join(process.cwd(), "generated-images"));
11975
+ fs2.mkdirSync(dir, { recursive: true });
11976
+ const file = path2.join(dir, `${slug}-${Date.now()}.${ext}`);
11977
+ fs2.writeFileSync(file, buffer);
11234
11978
  return { file, bytes: buffer.length, model: options?.model ?? DEFAULT_IMAGE_MODEL };
11235
11979
  }
11236
11980
  async imageGenerate(prompt, options) {
@@ -11740,8 +12484,8 @@ var init_episode_reasoning = __esm({
11740
12484
  });
11741
12485
 
11742
12486
  // ../core/dist/tools/runtime-tools.js
11743
- import * as fs2 from "fs";
11744
- import * as path2 from "path";
12487
+ import * as fs3 from "fs";
12488
+ import * as path3 from "path";
11745
12489
  function isEndTestAlreadyCalledError(value) {
11746
12490
  return /EndTest.*can only be called once|can only be called once/i.test(errorMessage(value));
11747
12491
  }
@@ -12626,9 +13370,9 @@ var init_runtime_tools = __esm({
12626
13370
  const rawJson = mutable.raw_json;
12627
13371
  if (typeof rawJson === "string") {
12628
13372
  if (typeof outputPath === "string" && outputPath !== "") {
12629
- const resolvedOutputPath = path2.resolve(outputPath);
12630
- fs2.mkdirSync(path2.dirname(resolvedOutputPath), { recursive: true });
12631
- fs2.writeFileSync(resolvedOutputPath, rawJson, "utf8");
13373
+ const resolvedOutputPath = path3.resolve(outputPath);
13374
+ fs3.mkdirSync(path3.dirname(resolvedOutputPath), { recursive: true });
13375
+ fs3.writeFileSync(resolvedOutputPath, rawJson, "utf8");
12632
13376
  mutable.output_path = resolvedOutputPath;
12633
13377
  }
12634
13378
  delete mutable.raw_json;
@@ -13341,6 +14085,7 @@ function buildCreateSoundLuau(options) {
13341
14085
  const lines = [
13342
14086
  `local parent = resolvePath(${luaString(options.parentPath)})`,
13343
14087
  `if parent == nil then error("Parent not found: " .. ${luaString(options.parentPath)}) end`,
14088
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
13344
14089
  `local sound = Instance.new("Sound")`,
13345
14090
  `sound.SoundId = ${luaString(assetUri(options.soundId))}`
13346
14091
  ];
@@ -13361,6 +14106,7 @@ function buildCreateSoundLuau(options) {
13361
14106
  function buildPlaySoundLuau(options) {
13362
14107
  const body = `local sound = resolvePath(${luaString(options.path)})
13363
14108
  if sound == nil or not sound:IsA("Sound") then error("Sound not found: " .. ${luaString(options.path)}) end
14109
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
13364
14110
  sound:Play()
13365
14111
  return { path = sound:GetFullName(), playing = true, success = true }`;
13366
14112
  return wrap(body);
@@ -13369,6 +14115,7 @@ function buildCreateAnimationLuau(options) {
13369
14115
  const lines = [
13370
14116
  `local parent = resolvePath(${luaString(options.parentPath)})`,
13371
14117
  `if parent == nil then error("Parent not found: " .. ${luaString(options.parentPath)}) end`,
14118
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
13372
14119
  `local anim = Instance.new("Animation")`,
13373
14120
  `anim.AnimationId = ${luaString(assetUri(options.animationId))}`,
13374
14121
  `anim.Name = ${luaString(options.name ?? "Animation")}`,
@@ -13387,6 +14134,7 @@ if animator == nil then
13387
14134
  animator = Instance.new("Animator")
13388
14135
  animator.Parent = controller
13389
14136
  end
14137
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
13390
14138
  local anim = Instance.new("Animation")
13391
14139
  anim.AnimationId = ${luaString(assetUri(options.animationId))}
13392
14140
  local track = animator:LoadAnimation(anim)
@@ -13400,6 +14148,7 @@ function buildApplyTextureLuau(options) {
13400
14148
  if (options.property) {
13401
14149
  const body2 = `local target = resolvePath(${luaString(options.targetPath)})
13402
14150
  if target == nil then error("Target not found: " .. ${luaString(options.targetPath)}) end
14151
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
13403
14152
  target.${options.property.replace(/[^A-Za-z0-9]/g, "")} = ${uri}
13404
14153
  return { path = target:GetFullName(), property = ${luaString(options.property)}, success = true }`;
13405
14154
  return wrap(body2);
@@ -13407,6 +14156,7 @@ return { path = target:GetFullName(), property = ${luaString(options.property)},
13407
14156
  const body = `local target = resolvePath(${luaString(options.targetPath)})
13408
14157
  if target == nil then error("Target not found: " .. ${luaString(options.targetPath)}) end
13409
14158
  local uri = ${uri}
14159
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
13410
14160
  local prop
13411
14161
  if target:IsA("ImageLabel") or target:IsA("ImageButton") then
13412
14162
  prop = "Image"
@@ -13446,6 +14196,7 @@ local parent = resolvePath(${luaString(parentPath)})
13446
14196
  if parent == nil then error("Parent not found: " .. ${luaString(parentPath)}) end
13447
14197
  local inputs = { ${inputs.join(", ")} }
13448
14198
  local schema = ${schemaLua}
14199
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
13449
14200
  local model, meta = GenerationService:GenerateModelAsync(inputs, schema)
13450
14201
  if model == nil then error("GenerateModelAsync returned no model") end
13451
14202
  local desiredName = ${nameLua}
@@ -13510,6 +14261,7 @@ local function ensureCorner(o)
13510
14261
  end
13511
14262
  end
13512
14263
  for _, o in ipairs(root:GetDescendants()) do
14264
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
13513
14265
  if o:IsA("GuiButton") then
13514
14266
  o.BackgroundColor3 = PRIMARY
13515
14267
  o.BorderSizePixel = 0
@@ -13590,6 +14342,7 @@ end
13590
14342
  local function lintRoot(root)
13591
14343
  local interactives = {}
13592
14344
  for _, o in ipairs(root:GetDescendants()) do
14345
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
13593
14346
  if o:IsA("GuiObject") then
13594
14347
  if (o:IsA("TextLabel") or o:IsA("TextButton") or o:IsA("TextBox")) and not o.TextScaled and o.TextSize < MIN_TEXT_SIZE then
13595
14348
  add("tiny_text", "warn", o, string.format("TextSize %d < %d; hard to read \u2014 raise it or use TextScaled + UITextSizeConstraint", o.TextSize, MIN_TEXT_SIZE))
@@ -13724,6 +14477,7 @@ function buildScreenGuiLuau(options) {
13724
14477
  const parentPath = options.parentPath ?? "StarterGui";
13725
14478
  const lines = [
13726
14479
  `local parent = resolvePath(${luaString(parentPath)}) or game:GetService("StarterGui")`,
14480
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
13727
14481
  `local obj = Instance.new("ScreenGui")`,
13728
14482
  `obj.Name = ${luaString(options.name)}`
13729
14483
  ];
@@ -13741,6 +14495,7 @@ function buildGuiObjectLuau(className, options) {
13741
14495
  const lines = [
13742
14496
  `local parent = resolvePath(${luaString(options.parentPath)})`,
13743
14497
  `if parent == nil then error("Parent not found: " .. ${luaString(options.parentPath)}) end`,
14498
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
13744
14499
  `local obj = Instance.new(${luaString(className)})`
13745
14500
  ];
13746
14501
  if (options.name !== void 0)
@@ -13783,6 +14538,7 @@ function buildApplyLayoutLuau(targetPath, options) {
13783
14538
  `local target = resolvePath(${luaString(targetPath)})`,
13784
14539
  `if target == nil then error("Target not found: " .. ${luaString(targetPath)}) end`
13785
14540
  ];
14541
+ lines.push("if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end");
13786
14542
  if (options.layout === "grid") {
13787
14543
  lines.push(`local layout = Instance.new("UIGridLayout")`);
13788
14544
  if (options.cellSize)
@@ -13816,6 +14572,7 @@ local function ensureScale(gui)
13816
14572
  scale.Parent = gui
13817
14573
  end
13818
14574
  end
14575
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
13819
14576
  local descendants = target:GetDescendants()
13820
14577
  table.insert(descendants, target)
13821
14578
  for _, child in ipairs(descendants) do
@@ -13848,7 +14605,10 @@ function clampClock(hour) {
13848
14605
  return Math.max(0, Math.min(24, hour));
13849
14606
  }
13850
14607
  function buildSetTimeOfDayLuau(time) {
13851
- const lines = ['local Lighting = game:GetService("Lighting")'];
14608
+ const lines = [
14609
+ 'local Lighting = game:GetService("Lighting")',
14610
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end"
14611
+ ];
13852
14612
  if (typeof time === "number") {
13853
14613
  lines.push(`Lighting.ClockTime = ${luaNumber(clampClock(time))}`);
13854
14614
  } else {
@@ -13898,6 +14658,7 @@ function buildLightingPresetLuau(preset, withPostFx = false) {
13898
14658
  }
13899
14659
  const lines = [
13900
14660
  'local Lighting = game:GetService("Lighting")',
14661
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
13901
14662
  `Lighting.ClockTime = ${luaNumber(config.clockTime)}`,
13902
14663
  `Lighting.Ambient = ${color3FromRGB(...config.ambient)}`,
13903
14664
  `Lighting.OutdoorAmbient = ${color3FromRGB(...config.outdoorAmbient)}`,
@@ -13919,7 +14680,10 @@ function buildLightingPresetLuau(preset, withPostFx = false) {
13919
14680
  return lines.join("\n");
13920
14681
  }
13921
14682
  function buildAtmosphereLuau(options) {
13922
- const lines = ['local Lighting = game:GetService("Lighting")'];
14683
+ const lines = [
14684
+ 'local Lighting = game:GetService("Lighting")',
14685
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end"
14686
+ ];
13923
14687
  lines.push(...atmosphereLines("Lighting", options));
13924
14688
  lines.push("return { success = true }");
13925
14689
  return lines.join("\n");
@@ -13927,6 +14691,7 @@ function buildAtmosphereLuau(options) {
13927
14691
  function buildSkyLuau(options) {
13928
14692
  const lines = [
13929
14693
  'local Lighting = game:GetService("Lighting")',
14694
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
13930
14695
  'local sky = Lighting:FindFirstChildOfClass("Sky") or Instance.new("Sky")'
13931
14696
  ];
13932
14697
  if (options.sunTextureId !== void 0)
@@ -13960,6 +14725,7 @@ RunService.Heartbeat:Connect(function(dt)
13960
14725
  end)`;
13961
14726
  const lines = [
13962
14727
  'local ServerScriptService = game:GetService("ServerScriptService")',
14728
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
13963
14729
  `local existing = ServerScriptService:FindFirstChild(${luaString(scriptName)})`,
13964
14730
  "if existing then existing:Destroy() end",
13965
14731
  'local script = Instance.new("Script")',
@@ -14072,6 +14838,7 @@ function buildBaseplateLuau(options) {
14072
14838
  const mat = material(options.material ?? "Grass");
14073
14839
  return [
14074
14840
  "local Terrain = workspace.Terrain",
14841
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
14075
14842
  `Terrain:FillBlock(CFrame.new(${vector3(...pos)}), ${vector3(...options.size)}, ${mat})`,
14076
14843
  `return { shape = "baseplate", volume = ${luaNumber(boxVolume(options.size))}, success = true }`
14077
14844
  ].join("\n");
@@ -14080,6 +14847,7 @@ function buildIslandLuau(options) {
14080
14847
  const mat = material(options.material ?? "Sand");
14081
14848
  const lines = [
14082
14849
  "local Terrain = workspace.Terrain",
14850
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
14083
14851
  `Terrain:FillBall(${vector3(...options.center)}, ${luaNumber(options.radius)}, ${mat})`
14084
14852
  ];
14085
14853
  if (options.waterMaterial || options.waterRadius) {
@@ -14104,7 +14872,9 @@ function buildMountainsLuau(options) {
14104
14872
  `local maxHeight = ${luaNumber(options.maxHeight)}`,
14105
14873
  `local seed = ${luaNumber(seed)}`,
14106
14874
  `local baseX, baseY, baseZ = ${luaNumber(cx - ex / 2)}, ${luaNumber(cy)}, ${luaNumber(cz - ez / 2)}`,
14107
- `for gx = 0, ${luaNumber(ex)}, res do`,
14875
+ `for gx = 0, ${luaNumber(ex)}, res do
14876
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end`,
14877
+ " if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
14108
14878
  ` for gz = 0, ${luaNumber(ez)}, res do`,
14109
14879
  " local wx = baseX + gx",
14110
14880
  " local wz = baseZ + gz",
@@ -14120,6 +14890,7 @@ function buildWaterLuau(options) {
14120
14890
  const pos = options.position ?? [0, 0, 0];
14121
14891
  return [
14122
14892
  "local Terrain = workspace.Terrain",
14893
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
14123
14894
  `Terrain:FillBlock(CFrame.new(${vector3(...pos)}), ${vector3(...options.size)}, Enum.Material.Water)`,
14124
14895
  `return { shape = "water", volume = ${luaNumber(boxVolume(options.size))}, success = true }`
14125
14896
  ].join("\n");
@@ -14130,6 +14901,7 @@ function buildPaintMaterialLuau(options) {
14130
14901
  const op = options.replaceMaterial ? `Terrain:ReplaceMaterial(${region}, 4, ${material(options.replaceMaterial)}, ${target})` : `Terrain:FillRegion(${region}, 4, ${target})`;
14131
14902
  return [
14132
14903
  "local Terrain = workspace.Terrain",
14904
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
14133
14905
  op,
14134
14906
  'return { shape = "paint", success = true }'
14135
14907
  ].join("\n");
@@ -14137,6 +14909,7 @@ function buildPaintMaterialLuau(options) {
14137
14909
  function buildClearRegionLuau(options) {
14138
14910
  return [
14139
14911
  "local Terrain = workspace.Terrain",
14912
+ "if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
14140
14913
  `Terrain:FillRegion(${region3(options.min, options.max)}, 4, Enum.Material.Air)`,
14141
14914
  'return { shape = "clear", success = true }'
14142
14915
  ].join("\n");
@@ -14217,6 +14990,8 @@ spawn.Anchored = true
14217
14990
 
14218
14991
  local checkpoints = ensure(course, "Checkpoints", "Folder")
14219
14992
  for i = 0, NUM_CHECKPOINTS do
14993
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
14994
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
14220
14995
  local cp = makePart(checkpoints, "Checkpoint" .. i, Vector3.new(10, 1, 10), Vector3.new(i * 40, 5, 0), Color3.fromRGB(80, 200, 120), true)
14221
14996
  cp:SetAttribute("Stage", i)
14222
14997
  if i > 0 then
@@ -14443,6 +15218,8 @@ local arena = ensure(Workspace, "Arena", "Model")
14443
15218
  makePart(arena, "ArenaFloor", Vector3.new(120, 1, 120), Vector3.new(300, 5, 0), Color3.fromRGB(110, 110, 120), true)
14444
15219
  local teleports = ensure(arena, "TeleportPoints", "Folder")
14445
15220
  for i = 1, NUM_TELEPORTS do
15221
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
15222
+ if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
14446
15223
  local angle = (i / NUM_TELEPORTS) * math.pi * 2
14447
15224
  local tp = makePart(teleports, "Teleport" .. i, Vector3.new(6, 1, 6), Vector3.new(300 + math.cos(angle) * 40, 6, math.sin(angle) * 40), Color3.fromRGB(120, 120, 255), true)
14448
15225
  tp.Transparency = 0.5
@@ -14709,8 +15486,8 @@ var init_sync_luau = __esm({
14709
15486
  });
14710
15487
 
14711
15488
  // ../core/dist/tools/sync-tools.js
14712
- import * as fs3 from "fs";
14713
- import * as path3 from "path";
15489
+ import * as fs4 from "fs";
15490
+ import * as path4 from "path";
14714
15491
  var SyncTools;
14715
15492
  var init_sync_tools = __esm({
14716
15493
  "../core/dist/tools/sync-tools.js"() {
@@ -14730,11 +15507,11 @@ var init_sync_tools = __esm({
14730
15507
  // clobbering. SyncManager owns the (tested) path/conflict logic; this layer
14731
15508
  // owns filesystem and Studio I/O.
14732
15509
  _syncManifestPath(dir) {
14733
- return path3.join(dir, ".robloxsync.json");
15510
+ return path4.join(dir, ".robloxsync.json");
14734
15511
  }
14735
15512
  _readManifest(dir) {
14736
15513
  try {
14737
- const raw = fs3.readFileSync(this._syncManifestPath(dir), "utf8");
15514
+ const raw = fs4.readFileSync(this._syncManifestPath(dir), "utf8");
14738
15515
  const parsed = JSON.parse(raw);
14739
15516
  return parsed && typeof parsed.paths === "object" ? parsed.paths : {};
14740
15517
  } catch {
@@ -14743,7 +15520,7 @@ var init_sync_tools = __esm({
14743
15520
  }
14744
15521
  _writeManifest(dir, paths) {
14745
15522
  const payload = { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), paths };
14746
- fs3.writeFileSync(this._syncManifestPath(dir), JSON.stringify(payload, null, 2));
15523
+ fs4.writeFileSync(this._syncManifestPath(dir), JSON.stringify(payload, null, 2));
14747
15524
  }
14748
15525
  async _dumpStudioScripts(instance_id) {
14749
15526
  const response = await this.runtime.callSingle("/api/execute-luau", { code: buildDumpScriptsLuau() }, "edit", instance_id);
@@ -14766,17 +15543,17 @@ var init_sync_tools = __esm({
14766
15543
  const walk = (current) => {
14767
15544
  let entries;
14768
15545
  try {
14769
- entries = fs3.readdirSync(current, { withFileTypes: true });
15546
+ entries = fs4.readdirSync(current, { withFileTypes: true });
14770
15547
  } catch {
14771
15548
  return;
14772
15549
  }
14773
15550
  for (const entry of entries) {
14774
- const full = path3.join(current, entry.name);
14775
- const rel = path3.relative(dir, full).split(path3.sep).join("/");
15551
+ const full = path4.join(current, entry.name);
15552
+ const rel = path4.relative(dir, full).split(path4.sep).join("/");
14776
15553
  if (entry.isDirectory()) {
14777
15554
  walk(full);
14778
15555
  } else if (this.sync.classNameForFile(entry.name) && !this.sync.isIgnored(rel)) {
14779
- out.set(rel, fs3.readFileSync(full, "utf8"));
15556
+ out.set(rel, fs4.readFileSync(full, "utf8"));
14780
15557
  }
14781
15558
  }
14782
15559
  };
@@ -14784,12 +15561,12 @@ var init_sync_tools = __esm({
14784
15561
  return out;
14785
15562
  }
14786
15563
  _resolveSyncDir(syncDir) {
14787
- return path3.resolve(syncDir ?? process.env.ROBLOX_SYNC_DIR ?? path3.join(process.cwd(), "roblox-src"));
15564
+ return path4.resolve(syncDir ?? process.env.ROBLOX_SYNC_DIR ?? path4.join(process.cwd(), "roblox-src"));
14788
15565
  }
14789
15566
  async syncPull(syncDir, instance_id) {
14790
15567
  const dir = this._resolveSyncDir(syncDir);
14791
15568
  const scripts = await this._dumpStudioScripts(instance_id);
14792
- fs3.mkdirSync(dir, { recursive: true });
15569
+ fs4.mkdirSync(dir, { recursive: true });
14793
15570
  const manifest = {};
14794
15571
  let written = 0;
14795
15572
  let skipped = 0;
@@ -14799,9 +15576,9 @@ var init_sync_tools = __esm({
14799
15576
  skipped++;
14800
15577
  continue;
14801
15578
  }
14802
- const full = path3.join(dir, rel);
14803
- fs3.mkdirSync(path3.dirname(full), { recursive: true });
14804
- fs3.writeFileSync(full, script.source);
15579
+ const full = path4.join(dir, rel);
15580
+ fs4.mkdirSync(path4.dirname(full), { recursive: true });
15581
+ fs4.writeFileSync(full, script.source);
14805
15582
  manifest[rel] = script.source;
14806
15583
  written++;
14807
15584
  }
@@ -15126,7 +15903,7 @@ var init_opencloud_client = __esm({
15126
15903
  if (result.error) {
15127
15904
  throw new Error(`Asset upload failed: ${result.error.message}`);
15128
15905
  }
15129
- await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
15906
+ await new Promise((resolve7) => setTimeout(resolve7, intervalMs));
15130
15907
  }
15131
15908
  throw new Error(`Asset upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
15132
15909
  }
@@ -15135,20 +15912,34 @@ var init_opencloud_client = __esm({
15135
15912
  });
15136
15913
 
15137
15914
  // ../core/dist/managed-instance-registry.js
15138
- import * as fs4 from "fs";
15139
- import * as os2 from "os";
15140
- import * as path4 from "path";
15915
+ import * as fs5 from "fs";
15916
+ import * as os3 from "os";
15917
+ import * as path5 from "path";
15141
15918
  function defaultManagedInstanceRegistryDir() {
15919
+ if (process.env.BLOXFORGE_MANAGED_INSTANCE_REGISTRY_DIR) {
15920
+ return process.env.BLOXFORGE_MANAGED_INSTANCE_REGISTRY_DIR;
15921
+ }
15142
15922
  if (process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR) {
15143
15923
  return process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR;
15144
15924
  }
15925
+ const suffix = path5.join("managed-instances", "v1");
15926
+ let newDir;
15927
+ let legacyDir;
15145
15928
  if (process.platform === "win32" && process.env.LOCALAPPDATA) {
15146
- return path4.join(process.env.LOCALAPPDATA, "robloxstudio-mcp", "managed-instances", "v1");
15147
- }
15148
- if (process.platform === "darwin") {
15149
- return path4.join(os2.homedir(), "Library", "Application Support", "robloxstudio-mcp", "managed-instances", "v1");
15929
+ newDir = path5.join(process.env.LOCALAPPDATA, "bloxforge", suffix);
15930
+ legacyDir = path5.join(process.env.LOCALAPPDATA, "robloxstudio-mcp", suffix);
15931
+ } else if (process.platform === "darwin") {
15932
+ newDir = path5.join(os3.homedir(), "Library", "Application Support", "bloxforge", suffix);
15933
+ legacyDir = path5.join(os3.homedir(), "Library", "Application Support", "robloxstudio-mcp", suffix);
15934
+ } else {
15935
+ newDir = path5.join(os3.homedir(), ".local", "state", "bloxforge", suffix);
15936
+ legacyDir = path5.join(os3.homedir(), ".local", "state", "robloxstudio-mcp", suffix);
15150
15937
  }
15151
- return path4.join(os2.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
15938
+ if (fs5.existsSync(newDir))
15939
+ return newDir;
15940
+ if (fs5.existsSync(legacyDir))
15941
+ return legacyDir;
15942
+ return newDir;
15152
15943
  }
15153
15944
  function sleepSync(ms) {
15154
15945
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -15228,20 +16019,20 @@ var init_managed_instance_registry = __esm({
15228
16019
  }
15229
16020
  withLock(fn) {
15230
16021
  this.ensureDir();
15231
- const lockDir = path4.join(this.dir, ".lock");
16022
+ const lockDir = path5.join(this.dir, ".lock");
15232
16023
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
15233
16024
  while (true) {
15234
16025
  try {
15235
- fs4.mkdirSync(lockDir);
16026
+ fs5.mkdirSync(lockDir);
15236
16027
  break;
15237
16028
  } catch (error) {
15238
16029
  const code = error.code;
15239
16030
  if (code !== "EEXIST")
15240
16031
  throw error;
15241
16032
  try {
15242
- const stat = fs4.statSync(lockDir);
16033
+ const stat = fs5.statSync(lockDir);
15243
16034
  if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
15244
- fs4.rmSync(lockDir, { recursive: true, force: true });
16035
+ fs5.rmSync(lockDir, { recursive: true, force: true });
15245
16036
  continue;
15246
16037
  }
15247
16038
  } catch {
@@ -15256,21 +16047,21 @@ var init_managed_instance_registry = __esm({
15256
16047
  try {
15257
16048
  return fn();
15258
16049
  } finally {
15259
- fs4.rmSync(lockDir, { recursive: true, force: true });
16050
+ fs5.rmSync(lockDir, { recursive: true, force: true });
15260
16051
  }
15261
16052
  }
15262
16053
  ensureDir() {
15263
- fs4.mkdirSync(this.dir, { recursive: true });
16054
+ fs5.mkdirSync(this.dir, { recursive: true });
15264
16055
  }
15265
16056
  recordPath(recordId) {
15266
- return path4.join(this.dir, `${recordId}.json`);
16057
+ return path5.join(this.dir, `${recordId}.json`);
15267
16058
  }
15268
16059
  recordFilesUnlocked() {
15269
- return fs4.readdirSync(this.dir).filter((name) => name.endsWith(".json")).map((name) => path4.join(this.dir, name));
16060
+ return fs5.readdirSync(this.dir).filter((name) => name.endsWith(".json")).map((name) => path5.join(this.dir, name));
15270
16061
  }
15271
16062
  readRecordUnlocked(recordId) {
15272
16063
  try {
15273
- const parsed = JSON.parse(fs4.readFileSync(this.recordPath(recordId), "utf8"));
16064
+ const parsed = JSON.parse(fs5.readFileSync(this.recordPath(recordId), "utf8"));
15274
16065
  return isRecord(parsed) ? parsed : void 0;
15275
16066
  } catch {
15276
16067
  return void 0;
@@ -15283,7 +16074,7 @@ var init_managed_instance_registry = __esm({
15283
16074
  const records = [];
15284
16075
  for (const file of this.recordFilesUnlocked()) {
15285
16076
  try {
15286
- const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
16077
+ const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
15287
16078
  if (!isRecord(parsed))
15288
16079
  continue;
15289
16080
  records.push(parsed);
@@ -15295,23 +16086,23 @@ var init_managed_instance_registry = __esm({
15295
16086
  writeRecordUnlocked(record) {
15296
16087
  this.ensureDir();
15297
16088
  const finalPath = this.recordPath(record.recordId);
15298
- const tmpPath = path4.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
15299
- const fd = fs4.openSync(tmpPath, "w");
16089
+ const tmpPath = path5.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
16090
+ const fd = fs5.openSync(tmpPath, "w");
15300
16091
  try {
15301
- fs4.writeFileSync(fd, `${JSON.stringify(record, null, 2)}
16092
+ fs5.writeFileSync(fd, `${JSON.stringify(record, null, 2)}
15302
16093
  `, "utf8");
15303
- fs4.fsyncSync(fd);
16094
+ fs5.fsyncSync(fd);
15304
16095
  } finally {
15305
- fs4.closeSync(fd);
16096
+ fs5.closeSync(fd);
15306
16097
  }
15307
- fs4.renameSync(tmpPath, finalPath);
16098
+ fs5.renameSync(tmpPath, finalPath);
15308
16099
  }
15309
16100
  deleteRecordUnlocked(recordId) {
15310
- fs4.rmSync(this.recordPath(recordId), { force: true });
16101
+ fs5.rmSync(this.recordPath(recordId), { force: true });
15311
16102
  }
15312
16103
  appendEventUnlocked(event, now) {
15313
- const file = path4.join(this.dir, `events-${ymd(now)}.jsonl`);
15314
- fs4.appendFileSync(file, `${JSON.stringify({
16104
+ const file = path5.join(this.dir, `events-${ymd(now)}.jsonl`);
16105
+ fs5.appendFileSync(file, `${JSON.stringify({
15315
16106
  ts: new Date(now).toISOString(),
15316
16107
  ...event
15317
16108
  })}
@@ -15319,12 +16110,12 @@ var init_managed_instance_registry = __esm({
15319
16110
  }
15320
16111
  cleanupOldEventLogsUnlocked(now) {
15321
16112
  const cutoff = ymd(now - EVENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
15322
- for (const name of fs4.readdirSync(this.dir)) {
16113
+ for (const name of fs5.readdirSync(this.dir)) {
15323
16114
  const date = eventLogDate(name);
15324
16115
  if (!date || date >= cutoff)
15325
16116
  continue;
15326
16117
  try {
15327
- fs4.rmSync(path4.join(this.dir, name), { force: true });
16118
+ fs5.rmSync(path5.join(this.dir, name), { force: true });
15328
16119
  } catch {
15329
16120
  }
15330
16121
  }
@@ -15348,9 +16139,9 @@ var init_managed_instance_registry = __esm({
15348
16139
  for (const file of this.recordFilesUnlocked()) {
15349
16140
  let parsed;
15350
16141
  try {
15351
- parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
16142
+ parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
15352
16143
  } catch {
15353
- fs4.rmSync(file, { force: true });
16144
+ fs5.rmSync(file, { force: true });
15354
16145
  this.appendEventUnlocked({
15355
16146
  event: "registry_pruned_malformed_record",
15356
16147
  reason: "parse_error",
@@ -15359,7 +16150,7 @@ var init_managed_instance_registry = __esm({
15359
16150
  continue;
15360
16151
  }
15361
16152
  if (!parsed || typeof parsed !== "object") {
15362
- fs4.rmSync(file, { force: true });
16153
+ fs5.rmSync(file, { force: true });
15363
16154
  this.appendEventUnlocked({
15364
16155
  event: "registry_pruned_malformed_record",
15365
16156
  reason: "invalid_shape",
@@ -15371,7 +16162,7 @@ var init_managed_instance_registry = __esm({
15371
16162
  if (typeof version === "number" && version > REGISTRY_VERSION)
15372
16163
  continue;
15373
16164
  if (!isRecord(parsed)) {
15374
- fs4.rmSync(file, { force: true });
16165
+ fs5.rmSync(file, { force: true });
15375
16166
  this.appendEventUnlocked({
15376
16167
  event: "registry_pruned_malformed_record",
15377
16168
  reason: "invalid_shape",
@@ -15380,7 +16171,7 @@ var init_managed_instance_registry = __esm({
15380
16171
  continue;
15381
16172
  }
15382
16173
  if (parsed.closedAt !== void 0) {
15383
- fs4.rmSync(file, { force: true });
16174
+ fs5.rmSync(file, { force: true });
15384
16175
  this.appendEventUnlocked({
15385
16176
  event: "registry_pruned_closed_record",
15386
16177
  recordId: parsed.recordId,
@@ -15393,7 +16184,7 @@ var init_managed_instance_registry = __esm({
15393
16184
  }
15394
16185
  if (parsed.bootId !== options.currentBootId) {
15395
16186
  this.cleanupRecord(options, parsed);
15396
- fs4.rmSync(file, { force: true });
16187
+ fs5.rmSync(file, { force: true });
15397
16188
  this.appendEventUnlocked({
15398
16189
  event: "registry_pruned_previous_boot",
15399
16190
  recordId: parsed.recordId,
@@ -15406,7 +16197,7 @@ var init_managed_instance_registry = __esm({
15406
16197
  }
15407
16198
  if (options.isProcessRunning && (parsed.nativeProcessId || parsed.spawnPid) && !options.isProcessRunning(parsed)) {
15408
16199
  this.cleanupRecord(options, parsed);
15409
- fs4.rmSync(file, { force: true });
16200
+ fs5.rmSync(file, { force: true });
15410
16201
  this.appendEventUnlocked({
15411
16202
  event: "registry_pruned_stale_process",
15412
16203
  recordId: parsed.recordId,
@@ -15424,10 +16215,10 @@ var init_managed_instance_registry = __esm({
15424
16215
 
15425
16216
  // ../core/dist/studio-instance-manager.js
15426
16217
  import { execFileSync, spawn } from "child_process";
15427
- import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
16218
+ import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync5, realpathSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
15428
16219
  import { randomUUID as randomUUID2 } from "crypto";
15429
- import * as os3 from "os";
15430
- import * as path5 from "path";
16220
+ import * as os4 from "os";
16221
+ import * as path6 from "path";
15431
16222
  function run(command, args, options = {}) {
15432
16223
  return execFileSync(command, args, {
15433
16224
  encoding: "utf8",
@@ -15439,14 +16230,14 @@ function isWsl() {
15439
16230
  if (process.platform !== "linux")
15440
16231
  return false;
15441
16232
  try {
15442
- return /microsoft|wsl/i.test(readFileSync4("/proc/version", "utf8"));
16233
+ return /microsoft|wsl/i.test(readFileSync5("/proc/version", "utf8"));
15443
16234
  } catch {
15444
16235
  return false;
15445
16236
  }
15446
16237
  }
15447
16238
  function powershell(script) {
15448
16239
  return run("powershell.exe", ["-NoProfile", "-Command", script], {
15449
- cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
16240
+ cwd: isWsl() && existsSync4("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
15450
16241
  });
15451
16242
  }
15452
16243
  function windowsLocalAppData() {
@@ -15456,7 +16247,7 @@ function windowsLocalAppData() {
15456
16247
  return void 0;
15457
16248
  try {
15458
16249
  return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
15459
- cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
16250
+ cwd: existsSync4("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
15460
16251
  });
15461
16252
  } catch {
15462
16253
  return void 0;
@@ -15468,7 +16259,7 @@ function toWslPath(windowsPath) {
15468
16259
  return run("wslpath", ["-u", windowsPath]);
15469
16260
  }
15470
16261
  function toStudioLaunchArg(arg) {
15471
- if (!isWsl() || !path5.isAbsolute(arg) || !existsSync2(arg))
16262
+ if (!isWsl() || !path6.isAbsolute(arg) || !existsSync4(arg))
15472
16263
  return arg;
15473
16264
  return run("wslpath", ["-w", arg]);
15474
16265
  }
@@ -15477,24 +16268,24 @@ function resolveEntrypointDir() {
15477
16268
  if (!entrypoint)
15478
16269
  return void 0;
15479
16270
  try {
15480
- return path5.dirname(realpathSync(entrypoint));
16271
+ return path6.dirname(realpathSync(entrypoint));
15481
16272
  } catch {
15482
- return path5.dirname(path5.resolve(entrypoint));
16273
+ return path6.dirname(path6.resolve(entrypoint));
15483
16274
  }
15484
16275
  }
15485
16276
  function resolveBaseplateTemplatePath() {
15486
16277
  const entrypointDir = resolveEntrypointDir();
15487
16278
  const candidates = [
15488
16279
  ...entrypointDir ? [
15489
- path5.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
15490
- path5.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
16280
+ path6.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
16281
+ path6.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
15491
16282
  ] : [],
15492
- path5.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
15493
- path5.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
15494
- path5.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
16283
+ path6.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
16284
+ path6.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
16285
+ path6.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
15495
16286
  ];
15496
16287
  for (const candidate of candidates) {
15497
- if (existsSync2(candidate))
16288
+ if (existsSync4(candidate))
15498
16289
  return candidate;
15499
16290
  }
15500
16291
  throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
@@ -15521,7 +16312,7 @@ function sweepStaleBaseplateFiles() {
15521
16312
  continue;
15522
16313
  if (Number(match[1]) !== process.pid && isProcessAlive(Number(match[1])))
15523
16314
  continue;
15524
- const file = path5.join(BASEPLATE_TEMP_DIR, entry);
16315
+ const file = path6.join(BASEPLATE_TEMP_DIR, entry);
15525
16316
  try {
15526
16317
  if (statSync3(file).mtimeMs < cutoff)
15527
16318
  rmSync2(file, { force: true });
@@ -15530,15 +16321,15 @@ function sweepStaleBaseplateFiles() {
15530
16321
  }
15531
16322
  }
15532
16323
  function createBaseplatePlaceFile() {
15533
- mkdirSync5(BASEPLATE_TEMP_DIR, { recursive: true });
16324
+ mkdirSync6(BASEPLATE_TEMP_DIR, { recursive: true });
15534
16325
  sweepStaleBaseplateFiles();
15535
- const file = path5.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
15536
- copyFileSync(resolveBaseplateTemplatePath(), file);
16326
+ const file = path6.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
16327
+ copyFileSync2(resolveBaseplateTemplatePath(), file);
15537
16328
  return file;
15538
16329
  }
15539
16330
  function isGeneratedBaseplatePlaceFile(file) {
15540
- const resolvedFile = path5.resolve(file);
15541
- return path5.dirname(resolvedFile) === path5.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path5.basename(resolvedFile));
16331
+ const resolvedFile = path6.resolve(file);
16332
+ return path6.dirname(resolvedFile) === path6.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path6.basename(resolvedFile));
15542
16333
  }
15543
16334
  function cleanupManagedBaseplateFiles(record) {
15544
16335
  if (record.source !== "baseplate" || !record.localPlaceFile)
@@ -15570,11 +16361,11 @@ function resolveStudioExe() {
15570
16361
  throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
15571
16362
  }
15572
16363
  const localAppData = windowsLocalAppData();
15573
- const root = localAppData ? path5.join(toWslPath(localAppData), "Roblox", "Versions") : path5.join(os3.homedir(), "AppData", "Local", "Roblox", "Versions");
15574
- if (!existsSync2(root)) {
16364
+ const root = localAppData ? path6.join(toWslPath(localAppData), "Roblox", "Versions") : path6.join(os4.homedir(), "AppData", "Local", "Roblox", "Versions");
16365
+ if (!existsSync4(root)) {
15575
16366
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
15576
16367
  }
15577
- const candidates = readdirSync4(root).filter((name) => name.startsWith("version-")).map((name) => path5.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
16368
+ const candidates = readdirSync4(root).filter((name) => name.startsWith("version-")).map((name) => path6.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync4(candidate)).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
15578
16369
  if (candidates.length === 0) {
15579
16370
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
15580
16371
  }
@@ -15609,7 +16400,7 @@ function listStudioProcesses() {
15609
16400
  function currentBootId() {
15610
16401
  if (process.platform === "linux") {
15611
16402
  try {
15612
- return readFileSync4("/proc/sys/kernel/random/boot_id", "utf8").trim();
16403
+ return readFileSync5("/proc/sys/kernel/random/boot_id", "utf8").trim();
15613
16404
  } catch {
15614
16405
  }
15615
16406
  }
@@ -15625,7 +16416,7 @@ function currentBootId() {
15625
16416
  } catch {
15626
16417
  }
15627
16418
  }
15628
- return `${process.platform}:${os3.hostname()}:unknown-boot`;
16419
+ return `${process.platform}:${os4.hostname()}:unknown-boot`;
15629
16420
  }
15630
16421
  function buildStudioLaunchArgs(options) {
15631
16422
  switch (options.source) {
@@ -15661,17 +16452,17 @@ function buildStudioLaunchArgs(options) {
15661
16452
  }
15662
16453
  }
15663
16454
  function delay(ms) {
15664
- return new Promise((resolve6) => setTimeout(resolve6, ms));
16455
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
15665
16456
  }
15666
16457
  function basenameAny(filePath) {
15667
- return path5.basename(filePath.replace(/\\/g, "/"));
16458
+ return path6.basename(filePath.replace(/\\/g, "/"));
15668
16459
  }
15669
16460
  var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
15670
16461
  var init_studio_instance_manager = __esm({
15671
16462
  "../core/dist/studio-instance-manager.js"() {
15672
16463
  "use strict";
15673
16464
  init_managed_instance_registry();
15674
- BASEPLATE_TEMP_DIR = path5.join(os3.tmpdir(), "robloxstudio-mcp-baseplates");
16465
+ BASEPLATE_TEMP_DIR = path6.join(os4.tmpdir(), "robloxstudio-mcp-baseplates");
15675
16466
  BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
15676
16467
  BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
15677
16468
  STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
@@ -15720,7 +16511,7 @@ var init_studio_instance_manager = __esm({
15720
16511
  const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
15721
16512
  const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
15722
16513
  const spawnOptions = {
15723
- cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
16514
+ cwd: isWsl() && existsSync4("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
15724
16515
  detached: true,
15725
16516
  stdio: "ignore"
15726
16517
  };
@@ -15926,13 +16717,13 @@ var init_studio_instance_manager = __esm({
15926
16717
  const processId = record.nativeProcessId ?? record.spawnPid;
15927
16718
  if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
15928
16719
  return true;
15929
- const processPath = studioProcess.Path ? path5.normalize(studioProcess.Path).toLowerCase() : "";
15930
- const exePath = record.exe ? path5.normalize(record.exe).toLowerCase() : "";
16720
+ const processPath = studioProcess.Path ? path6.normalize(studioProcess.Path).toLowerCase() : "";
16721
+ const exePath = record.exe ? path6.normalize(record.exe).toLowerCase() : "";
15931
16722
  if (processPath && exePath && (processPath === exePath || basenameAny(processPath) === basenameAny(exePath))) {
15932
16723
  return true;
15933
16724
  }
15934
16725
  const commandLine = studioProcess.CommandLine ?? "";
15935
- if (record.localPlaceFile && commandLine.includes(path5.basename(record.localPlaceFile)))
16726
+ if (record.localPlaceFile && commandLine.includes(path6.basename(record.localPlaceFile)))
15936
16727
  return true;
15937
16728
  if (record.placeId !== void 0 && commandLine.includes(String(record.placeId)))
15938
16729
  return true;
@@ -16133,10 +16924,207 @@ var init_session_recorder = __esm({
16133
16924
  }
16134
16925
  });
16135
16926
 
16927
+ // ../core/dist/quality-tools.js
16928
+ import { execFileSync as execFileSync2 } from "child_process";
16929
+ import * as fs6 from "fs";
16930
+ import * as os5 from "os";
16931
+ import * as path7 from "path";
16932
+ function within(root, candidate) {
16933
+ const relative3 = path7.relative(root, candidate);
16934
+ return relative3 === "" || !relative3.startsWith("..") && !path7.isAbsolute(relative3);
16935
+ }
16936
+ function allowedProjectRoot(root) {
16937
+ const candidate = path7.resolve(root);
16938
+ if (process.env.NODE_ENV === "test")
16939
+ return candidate;
16940
+ const allowed = path7.resolve(process.env.BLOXFORGE_PROJECT_ROOT?.trim() || process.cwd());
16941
+ if (!within(allowed, candidate))
16942
+ throw new Error(`Project root must stay within ${allowed}`);
16943
+ return candidate;
16944
+ }
16945
+ function hasCommand(command) {
16946
+ try {
16947
+ execFileSync2(command, ["--version"], { stdio: "pipe", timeout: 3e3 });
16948
+ return true;
16949
+ } catch (error) {
16950
+ return error?.code !== "ENOENT";
16951
+ }
16952
+ }
16953
+ function projectFiles(root) {
16954
+ const names = [
16955
+ "default.project.json",
16956
+ "rojo.json",
16957
+ "rojo.project.json",
16958
+ "sourcemap.json",
16959
+ "selene.toml",
16960
+ "stylua.toml",
16961
+ "wally.toml",
16962
+ "wally.lock",
16963
+ "rokit.toml",
16964
+ "aftman.toml"
16965
+ ];
16966
+ return Object.fromEntries(names.map((name) => [name, fs6.existsSync(path7.join(root, name)) ? path7.join(root, name) : void 0]).filter((entry) => entry[1] !== void 0));
16967
+ }
16968
+ function run2(command, args, options = {}) {
16969
+ if (!hasCommand(command))
16970
+ return { tool: command, available: false, ok: false, error: `${command} is not installed` };
16971
+ try {
16972
+ const output = execFileSync2(command, args, {
16973
+ cwd: options.cwd,
16974
+ input: options.input,
16975
+ encoding: "utf8",
16976
+ stdio: ["pipe", "pipe", "pipe"],
16977
+ timeout: 12e4,
16978
+ maxBuffer: 10 * 1024 * 1024
16979
+ });
16980
+ return { tool: command, available: true, ok: true, output: output.trim() };
16981
+ } catch (error) {
16982
+ return {
16983
+ tool: command,
16984
+ available: true,
16985
+ ok: false,
16986
+ output: [error?.stdout, error?.stderr].filter(Boolean).join("\n").trim(),
16987
+ error: error?.message ?? String(error),
16988
+ exitCode: typeof error?.status === "number" ? error.status : void 0
16989
+ };
16990
+ }
16991
+ }
16992
+ var TOOL_COMMANDS, QualityTools;
16993
+ var init_quality_tools = __esm({
16994
+ "../core/dist/quality-tools.js"() {
16995
+ "use strict";
16996
+ TOOL_COMMANDS = ["luau-analyze", "luau-lsp", "stylua", "selene", "rojo", "rokit", "wally", "lune"];
16997
+ QualityTools = class {
16998
+ detectRobloxProject(root = process.cwd()) {
16999
+ const boundary = process.env.NODE_ENV === "test" ? path7.parse(path7.resolve(root)).root : path7.resolve(process.env.BLOXFORGE_PROJECT_ROOT?.trim() || process.cwd());
17000
+ let current = allowedProjectRoot(root);
17001
+ while (true) {
17002
+ const files = projectFiles(current);
17003
+ if (Object.keys(files).length > 0) {
17004
+ return {
17005
+ root: current,
17006
+ files,
17007
+ availableTools: TOOL_COMMANDS.filter(hasCommand)
17008
+ };
17009
+ }
17010
+ const parent = path7.dirname(current);
17011
+ if (parent === current || !within(boundary, parent))
17012
+ break;
17013
+ current = parent;
17014
+ }
17015
+ return { root: path7.resolve(root), files: {}, availableTools: TOOL_COMMANDS.filter(hasCommand) };
17016
+ }
17017
+ validateScriptSource(source, fileName = "script.server.lua") {
17018
+ if (typeof source !== "string")
17019
+ throw new Error("source is required");
17020
+ const dir = fs6.mkdtempSync(path7.join(os5.tmpdir(), "bloxforge-quality-"));
17021
+ const file = path7.join(dir, path7.basename(fileName));
17022
+ fs6.writeFileSync(file, source, "utf8");
17023
+ const checks = [
17024
+ run2("luau-analyze", [file]),
17025
+ run2("selene", [file]),
17026
+ run2("stylua", ["--check", file])
17027
+ ];
17028
+ fs6.rmSync(dir, { recursive: true, force: true });
17029
+ return { checks };
17030
+ }
17031
+ formatScriptPreview(source, fileName = "script.server.lua") {
17032
+ if (typeof source !== "string")
17033
+ throw new Error("source is required");
17034
+ return run2("stylua", ["--stdin-filepath", fileName], { input: source });
17035
+ }
17036
+ resolveInstanceSourceFile(instancePath, root = process.cwd()) {
17037
+ const project = this.detectRobloxProject(root);
17038
+ const sourcemapPath = project.files["sourcemap.json"];
17039
+ if (!sourcemapPath)
17040
+ return { resolved: false, reason: "sourcemap.json not found", instancePath };
17041
+ let sourcemap;
17042
+ try {
17043
+ sourcemap = JSON.parse(fs6.readFileSync(sourcemapPath, "utf8"));
17044
+ } catch (error) {
17045
+ return { resolved: false, reason: `invalid sourcemap: ${error instanceof Error ? error.message : String(error)}`, instancePath };
17046
+ }
17047
+ const target = instancePath.split(".").filter(Boolean).reduce((node, segment) => {
17048
+ if (!node || typeof node !== "object")
17049
+ return void 0;
17050
+ return node.children?.find((child) => child.name === segment);
17051
+ }, sourcemap);
17052
+ return target ? { resolved: true, instancePath, node: target } : { resolved: false, instancePath, reason: "instance not found" };
17053
+ }
17054
+ getDependencyGraph(root = process.cwd()) {
17055
+ const project = this.detectRobloxProject(root);
17056
+ const lock = project.files["wally.lock"];
17057
+ const dependencies = [];
17058
+ if (lock) {
17059
+ for (const line of fs6.readFileSync(lock, "utf8").split(/\r?\n/)) {
17060
+ const match = line.match(/^\s*([\w.-]+)\s*=/);
17061
+ if (match && match[1] !== "version")
17062
+ dependencies.push(match[1]);
17063
+ }
17064
+ }
17065
+ return { root: project.root, manifest: project.files["wally.toml"], lockfile: lock, dependencies: [...new Set(dependencies)] };
17066
+ }
17067
+ installWallyPackages(root = process.cwd(), confirm = false) {
17068
+ if (!confirm)
17069
+ return { tool: "wally", available: hasCommand("wally"), ok: false, error: "Confirmation required: pass confirm=true to install packages" };
17070
+ return run2("wally", ["install"], { cwd: this.detectRobloxProject(root).root });
17071
+ }
17072
+ runProjectTests(root = process.cwd(), script) {
17073
+ if (!script)
17074
+ return { tool: "lune", available: hasCommand("lune"), ok: false, error: "script is required" };
17075
+ const project = this.detectRobloxProject(root);
17076
+ const scriptPath = path7.resolve(project.root, script);
17077
+ if (!within(project.root, scriptPath))
17078
+ return { tool: "lune", available: hasCommand("lune"), ok: false, error: "script must stay within project root" };
17079
+ return run2("lune", ["run", scriptPath], { cwd: project.root });
17080
+ }
17081
+ validateWithLuauLsp(root = process.cwd(), files = ["."]) {
17082
+ const project = this.detectRobloxProject(root);
17083
+ const args = ["analyze", ...files];
17084
+ if (project.files["sourcemap.json"])
17085
+ args.push("--sourcemap", project.files["sourcemap.json"]);
17086
+ return run2("luau-lsp", args, { cwd: project.root });
17087
+ }
17088
+ generateRojoSourcemap(root = process.cwd(), output = "sourcemap.json") {
17089
+ const project = this.detectRobloxProject(root);
17090
+ const projectFile = project.files["default.project.json"] ?? project.files["rojo.project.json"];
17091
+ if (!projectFile)
17092
+ return { tool: "rojo", available: hasCommand("rojo"), ok: false, error: "Rojo project file not found" };
17093
+ const outputPath = path7.resolve(project.root, output);
17094
+ if (!within(project.root, outputPath))
17095
+ return { tool: "rojo", available: hasCommand("rojo"), ok: false, error: "output must stay within project root" };
17096
+ return run2("rojo", ["sourcemap", projectFile, "--output", outputPath], { cwd: project.root });
17097
+ }
17098
+ buildRojoProject(root = process.cwd(), output) {
17099
+ const project = this.detectRobloxProject(root);
17100
+ const projectFile = project.files["default.project.json"] ?? project.files["rojo.project.json"];
17101
+ if (!projectFile)
17102
+ return { tool: "rojo", available: hasCommand("rojo"), ok: false, error: "Rojo project file not found" };
17103
+ if (!output)
17104
+ return { tool: "rojo", available: hasCommand("rojo"), ok: false, error: "output is required" };
17105
+ const outputPath = path7.resolve(project.root, output);
17106
+ if (!within(project.root, outputPath))
17107
+ return { tool: "rojo", available: hasCommand("rojo"), ok: false, error: "output must stay within project root" };
17108
+ return run2("rojo", ["build", projectFile, "--output", outputPath], { cwd: project.root });
17109
+ }
17110
+ runQualityGate(root = process.cwd()) {
17111
+ const project = this.detectRobloxProject(root);
17112
+ const checks = [
17113
+ run2("rojo", ["sourcemap"], { cwd: project.root }),
17114
+ run2("selene", ["."], { cwd: project.root }),
17115
+ run2("stylua", ["--check", "."], { cwd: project.root }),
17116
+ this.validateWithLuauLsp(project.root)
17117
+ ];
17118
+ return { project, checks };
17119
+ }
17120
+ };
17121
+ }
17122
+ });
17123
+
16136
17124
  // ../core/dist/tools/index.js
16137
- import * as fs5 from "fs";
16138
- import * as os4 from "os";
16139
- import * as path6 from "path";
17125
+ import * as fs7 from "fs";
17126
+ import * as os6 from "os";
17127
+ import * as path8 from "path";
16140
17128
  import * as crypto from "crypto";
16141
17129
  function requiresAttribution(license) {
16142
17130
  return /cc[\s-]?by|attribution/i.test(license ?? "");
@@ -16196,8 +17184,8 @@ function loadMicroProfilerBaseline(source, sourcePath) {
16196
17184
  if (sourcePath !== void 0) {
16197
17185
  if (typeof sourcePath !== "string" || sourcePath === "")
16198
17186
  throw new Error("baseline_path must be a non-empty string when provided");
16199
- const resolved = path6.resolve(sourcePath);
16200
- const parsed = JSON.parse(fs5.readFileSync(resolved, "utf8"));
17187
+ const resolved = path8.resolve(sourcePath);
17188
+ const parsed = JSON.parse(fs7.readFileSync(resolved, "utf8"));
16201
17189
  const record = asRecord(parsed);
16202
17190
  if (!record)
16203
17191
  throw new Error(`baseline_path did not contain a JSON object: ${resolved}`);
@@ -16329,6 +17317,7 @@ var init_tools = __esm({
16329
17317
  init_roblox_cookie_client();
16330
17318
  init_roblox_docs();
16331
17319
  init_session_recorder();
17320
+ init_quality_tools();
16332
17321
  init_runtime_support();
16333
17322
  RobloxStudioTools = class _RobloxStudioTools {
16334
17323
  client;
@@ -16351,6 +17340,7 @@ var init_tools = __esm({
16351
17340
  runtimeTools;
16352
17341
  episodes;
16353
17342
  sessionRecorder;
17343
+ qualityTools;
16354
17344
  /** Provenance for externally-imported assets (Track A) — source/license/hash/assetId. */
16355
17345
  provenance = /* @__PURE__ */ new Map();
16356
17346
  constructor(bridge) {
@@ -16388,7 +17378,7 @@ var init_tools = __esm({
16388
17378
  this.scriptTools = new ScriptTools({
16389
17379
  callSingle: this._callSingle.bind(this),
16390
17380
  safetyGate: this._safetyGate.bind(this),
16391
- backupScript: (path7, source) => this.safety.backupScript(path7, source),
17381
+ backupScript: (path9, source) => this.safety.backupScript(path9, source),
16392
17382
  recordOperation: (kind, summary) => this.safety.recordOperation({ kind, summary })
16393
17383
  });
16394
17384
  this.mutationTools = new MutationTools({
@@ -16407,6 +17397,7 @@ var init_tools = __esm({
16407
17397
  });
16408
17398
  this.episodes = new EpisodeStore();
16409
17399
  this.sessionRecorder = new SessionRecorder();
17400
+ this.qualityTools = new QualityTools();
16410
17401
  this.runtimeTools = new RuntimeTools({
16411
17402
  bridge: this.bridge,
16412
17403
  client: this.client,
@@ -16427,6 +17418,53 @@ var init_tools = __esm({
16427
17418
  async getSessionSummary() {
16428
17419
  return wrapToolJsonText(this.sessionRecorder.summarize());
16429
17420
  }
17421
+ async getRequestStatus(requestId) {
17422
+ if (!requestId)
17423
+ throw new Error("requestId is required for get_request_status");
17424
+ const status = await this.bridge.lookupRequestStatus(requestId);
17425
+ return wrapToolJsonText(status ?? { error: "request_not_found", requestId });
17426
+ }
17427
+ async getTransportDiagnostics() {
17428
+ return wrapToolJsonText(this.bridge.getTransportDiagnostics());
17429
+ }
17430
+ async cancelRequest(requestId) {
17431
+ if (!requestId)
17432
+ throw new Error("requestId is required for cancel_request");
17433
+ return wrapToolJsonText({ requestId, cancelled: await this.bridge.requestCancellation(requestId) });
17434
+ }
17435
+ async detectRobloxProject(root) {
17436
+ return wrapToolJsonText(this.qualityTools.detectRobloxProject(root));
17437
+ }
17438
+ async validateScriptSource(source, fileName) {
17439
+ return wrapToolJsonText(this.qualityTools.validateScriptSource(source, fileName));
17440
+ }
17441
+ async formatScriptPreview(source, fileName) {
17442
+ return wrapToolJsonText(this.qualityTools.formatScriptPreview(source, fileName));
17443
+ }
17444
+ async resolveInstanceSourceFile(instancePath, root) {
17445
+ return wrapToolJsonText(this.qualityTools.resolveInstanceSourceFile(instancePath, root));
17446
+ }
17447
+ async runProjectTests(root, script) {
17448
+ return wrapToolJsonText(this.qualityTools.runProjectTests(root, script));
17449
+ }
17450
+ async getDependencyGraph(root) {
17451
+ return wrapToolJsonText(this.qualityTools.getDependencyGraph(root));
17452
+ }
17453
+ async installWallyPackages(root, confirm) {
17454
+ return wrapToolJsonText(this.qualityTools.installWallyPackages(root, confirm));
17455
+ }
17456
+ async runQualityGate(root) {
17457
+ return wrapToolJsonText(this.qualityTools.runQualityGate(root));
17458
+ }
17459
+ async validateWithLuauLsp(root, files) {
17460
+ return wrapToolJsonText(this.qualityTools.validateWithLuauLsp(root, files));
17461
+ }
17462
+ async generateRojoSourcemap(root, output) {
17463
+ return wrapToolJsonText(this.qualityTools.generateRojoSourcemap(root, output));
17464
+ }
17465
+ async buildRojoProject(root, output) {
17466
+ return wrapToolJsonText(this.qualityTools.buildRojoProject(root, output));
17467
+ }
16430
17468
  async getRobloxDocs(name, docType, section) {
16431
17469
  if (!name || typeof name !== "string") {
16432
17470
  throw new Error('get_roblox_docs requires a name (e.g. "ProximityPrompt")');
@@ -16604,8 +17642,8 @@ var init_tools = __esm({
16604
17642
  }
16605
17643
  // Scene-read inspection tools live in SceneReadTools; the facade delegates with
16606
17644
  // identical signatures.
16607
- async getFileTree(path7 = "", instance_id) {
16608
- return this.sceneReadTools.getFileTree(path7, instance_id);
17645
+ async getFileTree(path9 = "", instance_id) {
17646
+ return this.sceneReadTools.getFileTree(path9, instance_id);
16609
17647
  }
16610
17648
  async searchFiles(query, searchType = "name", instance_id) {
16611
17649
  const response = await this._callSingle("/api/search-files", { query, searchType }, void 0, instance_id);
@@ -16639,8 +17677,8 @@ var init_tools = __esm({
16639
17677
  async getClassInfo(className, instance_id) {
16640
17678
  return this.sceneReadTools.getClassInfo(className, instance_id);
16641
17679
  }
16642
- async getProjectStructure(path7, maxDepth, scriptsOnly, instance_id) {
16643
- return this.sceneReadTools.getProjectStructure(path7, maxDepth, scriptsOnly, instance_id);
17680
+ async getProjectStructure(path9, maxDepth, scriptsOnly, instance_id) {
17681
+ return this.sceneReadTools.getProjectStructure(path9, maxDepth, scriptsOnly, instance_id);
16644
17682
  }
16645
17683
  // Mutation tools live in MutationTools; the facade delegates with identical signatures.
16646
17684
  async setProperty(instancePath, propertyName, propertyValue, instance_id) {
@@ -16824,14 +17862,14 @@ var init_tools = __esm({
16824
17862
  return this.runtimeTools.redo(instance_id);
16825
17863
  }
16826
17864
  static findProjectRoot(startDir) {
16827
- let dir = path6.resolve(startDir);
17865
+ let dir = path8.resolve(startDir);
16828
17866
  let previous = "";
16829
17867
  while (dir !== previous) {
16830
- if (fs5.existsSync(path6.join(dir, ".git")) || fs5.existsSync(path6.join(dir, "package.json"))) {
17868
+ if (fs7.existsSync(path8.join(dir, ".git")) || fs7.existsSync(path8.join(dir, "package.json"))) {
16831
17869
  return dir;
16832
17870
  }
16833
17871
  previous = dir;
16834
- dir = path6.dirname(dir);
17872
+ dir = path8.dirname(dir);
16835
17873
  }
16836
17874
  return null;
16837
17875
  }
@@ -16839,15 +17877,15 @@ var init_tools = __esm({
16839
17877
  if (!candidate)
16840
17878
  return false;
16841
17879
  try {
16842
- return fs5.statSync(candidate).isDirectory();
17880
+ return fs7.statSync(candidate).isDirectory();
16843
17881
  } catch {
16844
17882
  return false;
16845
17883
  }
16846
17884
  }
16847
17885
  static ensureWritableDirectory(candidate, label) {
16848
- const resolved = path6.resolve(candidate);
17886
+ const resolved = path8.resolve(candidate);
16849
17887
  try {
16850
- fs5.mkdirSync(resolved, { recursive: true });
17888
+ fs7.mkdirSync(resolved, { recursive: true });
16851
17889
  } catch (error) {
16852
17890
  throw new Error(`Unable to create ${label} build-library directory at ${resolved}: ${error.message}`);
16853
17891
  }
@@ -16855,7 +17893,7 @@ var init_tools = __esm({
16855
17893
  throw new Error(`${label} build-library path is not a directory: ${resolved}`);
16856
17894
  }
16857
17895
  try {
16858
- fs5.accessSync(resolved, fs5.constants.W_OK);
17896
+ fs7.accessSync(resolved, fs7.constants.W_OK);
16859
17897
  } catch (error) {
16860
17898
  throw new Error(`${label} build-library directory is not writable: ${resolved}. ${error.message}`);
16861
17899
  }
@@ -16865,26 +17903,28 @@ var init_tools = __esm({
16865
17903
  static findLibraryPath() {
16866
17904
  if (_RobloxStudioTools._cachedLibraryPath)
16867
17905
  return _RobloxStudioTools._cachedLibraryPath;
16868
- const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
16869
- const cwd = path6.resolve(process.cwd());
17906
+ const overridePath = process.env.BLOXFORGE_BUILD_LIBRARY || process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
17907
+ const cwd = path8.resolve(process.cwd());
16870
17908
  const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
16871
- const homeLibraryPath = path6.join(os4.homedir(), ".robloxstudio-mcp", "build-library");
16872
- const projectLibraryPath = projectRoot ? path6.join(projectRoot, "build-library") : null;
16873
- const cwdLibraryPath = path6.join(cwd, "build-library");
17909
+ const newHomeLibraryPath = path8.join(os6.homedir(), ".bloxforge", "build-library");
17910
+ const legacyHomeLibraryPath = path8.join(os6.homedir(), ".robloxstudio-mcp", "build-library");
17911
+ const homeLibraryPath = fs7.existsSync(newHomeLibraryPath) ? newHomeLibraryPath : fs7.existsSync(legacyHomeLibraryPath) ? legacyHomeLibraryPath : newHomeLibraryPath;
17912
+ const projectLibraryPath = projectRoot ? path8.join(projectRoot, "build-library") : null;
17913
+ const cwdLibraryPath = path8.join(cwd, "build-library");
16874
17914
  let result;
16875
17915
  if (overridePath) {
16876
17916
  result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
16877
17917
  } else {
16878
17918
  const existing = [projectLibraryPath, cwdLibraryPath].find((c) => c && _RobloxStudioTools.isDirectory(c) && (() => {
16879
17919
  try {
16880
- fs5.accessSync(c, fs5.constants.W_OK);
17920
+ fs7.accessSync(c, fs7.constants.W_OK);
16881
17921
  return true;
16882
17922
  } catch {
16883
17923
  return false;
16884
17924
  }
16885
17925
  })());
16886
17926
  if (existing) {
16887
- result = path6.resolve(existing);
17927
+ result = path8.resolve(existing);
16888
17928
  } else if (projectLibraryPath) {
16889
17929
  try {
16890
17930
  result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
@@ -16911,12 +17951,12 @@ var init_tools = __esm({
16911
17951
  if (response && response.success && response.buildData) {
16912
17952
  const buildData = response.buildData;
16913
17953
  const buildId = buildData.id || `${style}/exported`;
16914
- const filePath = path6.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
16915
- const dirPath = path6.dirname(filePath);
16916
- if (!fs5.existsSync(dirPath)) {
16917
- fs5.mkdirSync(dirPath, { recursive: true });
17954
+ const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
17955
+ const dirPath = path8.dirname(filePath);
17956
+ if (!fs7.existsSync(dirPath)) {
17957
+ fs7.mkdirSync(dirPath, { recursive: true });
16918
17958
  }
16919
- fs5.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
17959
+ fs7.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
16920
17960
  response.savedTo = filePath;
16921
17961
  }
16922
17962
  return {
@@ -17013,12 +18053,12 @@ var init_tools = __esm({
17013
18053
  const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
17014
18054
  const computedBounds = bounds || this.computeBounds(normalizedParts);
17015
18055
  const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
17016
- const filePath = path6.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
17017
- const dirPath = path6.dirname(filePath);
17018
- if (!fs5.existsSync(dirPath)) {
17019
- fs5.mkdirSync(dirPath, { recursive: true });
18056
+ const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
18057
+ const dirPath = path8.dirname(filePath);
18058
+ if (!fs7.existsSync(dirPath)) {
18059
+ fs7.mkdirSync(dirPath, { recursive: true });
17020
18060
  }
17021
- fs5.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
18061
+ fs7.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
17022
18062
  return {
17023
18063
  content: [
17024
18064
  {
@@ -17072,12 +18112,12 @@ var init_tools = __esm({
17072
18112
  };
17073
18113
  if (seed !== void 0)
17074
18114
  buildData.generatorSeed = seed;
17075
- const filePath = path6.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
17076
- const dirPath = path6.dirname(filePath);
17077
- if (!fs5.existsSync(dirPath)) {
17078
- fs5.mkdirSync(dirPath, { recursive: true });
18115
+ const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
18116
+ const dirPath = path8.dirname(filePath);
18117
+ if (!fs7.existsSync(dirPath)) {
18118
+ fs7.mkdirSync(dirPath, { recursive: true });
17079
18119
  }
17080
- fs5.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
18120
+ fs7.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
17081
18121
  return {
17082
18122
  content: [
17083
18123
  {
@@ -17101,17 +18141,17 @@ var init_tools = __esm({
17101
18141
  }
17102
18142
  let resolved;
17103
18143
  if (typeof buildData === "string") {
17104
- const filePath = path6.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
17105
- if (!fs5.existsSync(filePath)) {
18144
+ const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
18145
+ if (!fs7.existsSync(filePath)) {
17106
18146
  throw new Error(`Build not found in library: ${buildData}`);
17107
18147
  }
17108
- resolved = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
18148
+ resolved = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
17109
18149
  } else if (buildData.id && !buildData.parts) {
17110
- const filePath = path6.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
17111
- if (!fs5.existsSync(filePath)) {
18150
+ const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
18151
+ if (!fs7.existsSync(filePath)) {
17112
18152
  throw new Error(`Build not found in library: ${buildData.id}`);
17113
18153
  }
17114
- resolved = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
18154
+ resolved = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
17115
18155
  } else {
17116
18156
  resolved = buildData;
17117
18157
  }
@@ -17134,13 +18174,13 @@ var init_tools = __esm({
17134
18174
  const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
17135
18175
  const builds = [];
17136
18176
  for (const s of styles) {
17137
- const dirPath = path6.join(libraryPath, s);
17138
- if (!fs5.existsSync(dirPath))
18177
+ const dirPath = path8.join(libraryPath, s);
18178
+ if (!fs7.existsSync(dirPath))
17139
18179
  continue;
17140
- const files = fs5.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
18180
+ const files = fs7.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
17141
18181
  for (const file of files) {
17142
18182
  try {
17143
- const content = fs5.readFileSync(path6.join(dirPath, file), "utf-8");
18183
+ const content = fs7.readFileSync(path8.join(dirPath, file), "utf-8");
17144
18184
  const data = JSON.parse(content);
17145
18185
  builds.push({
17146
18186
  id: data.id || `${s}/${file.replace(".json", "")}`,
@@ -17179,11 +18219,11 @@ var init_tools = __esm({
17179
18219
  if (!id) {
17180
18220
  throw new Error("Build ID is required for get_build");
17181
18221
  }
17182
- const filePath = path6.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
17183
- if (!fs5.existsSync(filePath)) {
18222
+ const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
18223
+ if (!fs7.existsSync(filePath)) {
17184
18224
  throw new Error(`Build not found in library: ${id}`);
17185
18225
  }
17186
- const data = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
18226
+ const data = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
17187
18227
  const result = {
17188
18228
  id: data.id,
17189
18229
  style: data.style,
@@ -17266,11 +18306,11 @@ var init_tools = __esm({
17266
18306
  if (!buildId) {
17267
18307
  throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
17268
18308
  }
17269
- const filePath = path6.join(libraryPath, `${buildId}.json`);
17270
- if (!fs5.existsSync(filePath)) {
18309
+ const filePath = path8.join(libraryPath, `${buildId}.json`);
18310
+ if (!fs7.existsSync(filePath)) {
17271
18311
  throw new Error(`Build not found in library: ${buildId}`);
17272
18312
  }
17273
- const content = fs5.readFileSync(filePath, "utf-8");
18313
+ const content = fs7.readFileSync(filePath, "utf-8");
17274
18314
  const buildData = JSON.parse(content);
17275
18315
  const buildName = buildId.split("/").pop() || buildId;
17276
18316
  expandedBuilds.push({
@@ -17585,10 +18625,10 @@ var init_tools = __esm({
17585
18625
  this.safety.recordOperation({ kind: "audio", summary: `sound ${options.soundId} under ${options.parentPath}` });
17586
18626
  return result;
17587
18627
  }
17588
- async audioPlaySound(path7, instance_id) {
17589
- if (!path7)
18628
+ async audioPlaySound(path9, instance_id) {
18629
+ if (!path9)
17590
18630
  throw new Error("path is required for audio_play_sound");
17591
- return this._runGeneratedLuau(buildPlaySoundLuau({ path: path7 }), instance_id);
18631
+ return this._runGeneratedLuau(buildPlaySoundLuau({ path: path9 }), instance_id);
17592
18632
  }
17593
18633
  async animationCreate(options, instance_id) {
17594
18634
  if (!options?.parentPath || options?.animationId === void 0)
@@ -17695,21 +18735,21 @@ var init_tools = __esm({
17695
18735
  if (!res.ok)
17696
18736
  return { content: [{ type: "text", text: JSON.stringify({ error: `Download failed: HTTP ${res.status} for ${options.source}` }) }] };
17697
18737
  const buf = Buffer.from(await res.arrayBuffer());
17698
- const ext = path6.extname(new URL(options.source).pathname) || ".bin";
17699
- filePath = path6.join(os4.tmpdir(), `mcp-ext-${Date.now()}${ext}`);
17700
- fs5.writeFileSync(filePath, buf);
18738
+ const ext = path8.extname(new URL(options.source).pathname) || ".bin";
18739
+ filePath = path8.join(os6.tmpdir(), `mcp-ext-${Date.now()}${ext}`);
18740
+ fs7.writeFileSync(filePath, buf);
17701
18741
  bytes = buf.length;
17702
18742
  cleanup = true;
17703
18743
  } else {
17704
- if (!fs5.existsSync(options.source))
18744
+ if (!fs7.existsSync(options.source))
17705
18745
  throw new Error(`File not found: ${options.source}`);
17706
18746
  filePath = options.source;
17707
- bytes = fs5.statSync(filePath).size;
18747
+ bytes = fs7.statSync(filePath).size;
17708
18748
  }
17709
- const sha256 = crypto.createHash("sha256").update(fs5.readFileSync(filePath)).digest("hex");
18749
+ const sha256 = crypto.createHash("sha256").update(fs7.readFileSync(filePath)).digest("hex");
17710
18750
  let uploadRaw = {};
17711
18751
  try {
17712
- const up = await this.uploadAsset(filePath, assetType, options.displayName ?? options.sourceName ?? path6.basename(filePath));
18752
+ const up = await this.uploadAsset(filePath, assetType, options.displayName ?? options.sourceName ?? path8.basename(filePath));
17713
18753
  const text = up.content?.find((c) => "text" in c)?.text ?? "{}";
17714
18754
  uploadRaw = JSON.parse(text);
17715
18755
  } catch (error) {
@@ -17717,7 +18757,7 @@ var init_tools = __esm({
17717
18757
  } finally {
17718
18758
  if (cleanup) {
17719
18759
  try {
17720
- fs5.unlinkSync(filePath);
18760
+ fs7.unlinkSync(filePath);
17721
18761
  } catch {
17722
18762
  }
17723
18763
  }
@@ -17776,10 +18816,10 @@ var init_tools = __esm({
17776
18816
  const { buffer, contentType } = await this.imageClient.generate(prompt, options ?? {});
17777
18817
  const ext = contentType.includes("png") ? "png" : "jpg";
17778
18818
  const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "image";
17779
- const dir = path6.resolve(process.env.ROBLOX_IMAGE_DIR ?? path6.join(process.cwd(), "generated-images"));
17780
- fs5.mkdirSync(dir, { recursive: true });
17781
- const file = path6.join(dir, `${slug}-${Date.now()}.${ext}`);
17782
- fs5.writeFileSync(file, buffer);
18819
+ const dir = path8.resolve(process.env.ROBLOX_IMAGE_DIR ?? path8.join(process.cwd(), "generated-images"));
18820
+ fs7.mkdirSync(dir, { recursive: true });
18821
+ const file = path8.join(dir, `${slug}-${Date.now()}.${ext}`);
18822
+ fs7.writeFileSync(file, buffer);
17783
18823
  return { file, bytes: buffer.length, model: options?.model ?? DEFAULT_IMAGE_MODEL };
17784
18824
  }
17785
18825
  async imageGenerate(prompt, options) {
@@ -17845,11 +18885,11 @@ var init_tools = __esm({
17845
18885
  return null;
17846
18886
  }
17847
18887
  async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
17848
- if (!fs5.existsSync(filePath)) {
18888
+ if (!fs7.existsSync(filePath)) {
17849
18889
  throw new Error(`File not found: ${filePath}`);
17850
18890
  }
17851
- const fileContent = fs5.readFileSync(filePath);
17852
- const fileName = path6.basename(filePath);
18891
+ const fileContent = fs7.readFileSync(filePath);
18892
+ const fileName = path8.basename(filePath);
17853
18893
  if (assetType === "Decal" && this.cookieClient.hasCookie()) {
17854
18894
  const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
17855
18895
  return {
@@ -17928,8 +18968,8 @@ var init_tools = __esm({
17928
18968
  async getSceneSummary(instancePath, topN, instance_id) {
17929
18969
  return this.sceneReadTools.getSceneSummary(instancePath, topN, instance_id);
17930
18970
  }
17931
- async applyMutationPlan(operations, dryRun, confirm, instance_id) {
17932
- return this.mutationTools.applyMutationPlan(operations, dryRun, confirm, instance_id);
18971
+ async applyMutationPlan(operations, dryRun, confirm, instance_id, atomic) {
18972
+ return this.mutationTools.applyMutationPlan(operations, dryRun, confirm, instance_id, atomic);
17933
18973
  }
17934
18974
  // Recipes: proven idempotent build macros. list is pure; apply runs the recipe's
17935
18975
  // Luau (creates/replaces named instances).
@@ -17976,7 +19016,7 @@ var init_tools = __esm({
17976
19016
  // state is this and how did it get here". Also readable at roblox://repro/bundle.
17977
19017
  // Track G2. ponytail: composes existing readers; no new persistence.
17978
19018
  async getReproductionBundle(instance_id) {
17979
- const parse = (r) => {
19019
+ const parse2 = (r) => {
17980
19020
  try {
17981
19021
  const t = r?.content?.[0]?.text;
17982
19022
  return typeof t === "string" ? JSON.parse(t) : {};
@@ -17985,10 +19025,10 @@ var init_tools = __esm({
17985
19025
  }
17986
19026
  };
17987
19027
  const [snapshot, recentOperations, instances, episodes] = await Promise.all([
17988
- this.getWorldSnapshot(void 0, "overview", void 0, instance_id).then(parse).catch(() => ({})),
17989
- Promise.resolve(this.getOperationHistory(25)).then(parse).catch(() => ({})),
17990
- Promise.resolve(this.getConnectedInstances()).then(parse).catch(() => ({})),
17991
- Promise.resolve(this.listEpisodes()).then(parse).catch(() => ({}))
19028
+ this.getWorldSnapshot(void 0, "overview", void 0, instance_id).then(parse2).catch(() => ({})),
19029
+ Promise.resolve(this.getOperationHistory(25)).then(parse2).catch(() => ({})),
19030
+ Promise.resolve(this.getConnectedInstances()).then(parse2).catch(() => ({})),
19031
+ Promise.resolve(this.listEpisodes()).then(parse2).catch(() => ({}))
17992
19032
  ]);
17993
19033
  return { content: [{ type: "text", text: JSON.stringify({
17994
19034
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -18008,17 +19048,17 @@ var init_tools = __esm({
18008
19048
  async toolCatalogSearch(body) {
18009
19049
  return this.discoveryTools.toolCatalogSearch(body);
18010
19050
  }
18011
- async getWorldSnapshot(path7, level, topNPerClass, instance_id) {
18012
- return this.worldTools.getWorldSnapshot(path7, level, topNPerClass, instance_id);
19051
+ async getWorldSnapshot(path9, level, topNPerClass, instance_id) {
19052
+ return this.worldTools.getWorldSnapshot(path9, level, topNPerClass, instance_id);
18013
19053
  }
18014
- async sceneSearch(query, path7, limit, instance_id) {
18015
- return this.worldTools.sceneSearch(query, path7, limit, instance_id);
19054
+ async sceneSearch(query, path9, limit, instance_id) {
19055
+ return this.worldTools.sceneSearch(query, path9, limit, instance_id);
18016
19056
  }
18017
19057
  async getNodeBatch(paths, fields, includeChildrenCount, instance_id) {
18018
19058
  return this.worldTools.getNodeBatch(paths, fields, includeChildrenCount, instance_id);
18019
19059
  }
18020
- async getChangesSince(snapshotId, path7, instance_id) {
18021
- return this.worldTools.getChangesSince(snapshotId, path7, instance_id);
19060
+ async getChangesSince(snapshotId, path9, instance_id) {
19061
+ return this.worldTools.getChangesSince(snapshotId, path9, instance_id);
18022
19062
  }
18023
19063
  async assetPreflightInsert(assetId, instance_id) {
18024
19064
  return this.worldTools.assetPreflightInsert(assetId, instance_id);
@@ -18057,10 +19097,10 @@ var init_tools = __esm({
18057
19097
  return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
18058
19098
  }
18059
19099
  const bytes = Buffer.from(response.base64, "base64");
18060
- const resolved = path6.resolve(outputPath);
19100
+ const resolved = path8.resolve(outputPath);
18061
19101
  try {
18062
- fs5.mkdirSync(path6.dirname(resolved), { recursive: true });
18063
- fs5.writeFileSync(resolved, bytes);
19102
+ fs7.mkdirSync(path8.dirname(resolved), { recursive: true });
19103
+ fs7.writeFileSync(resolved, bytes);
18064
19104
  } catch (err) {
18065
19105
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
18066
19106
  }
@@ -18093,9 +19133,9 @@ var init_tools = __esm({
18093
19133
  let bytes;
18094
19134
  let sourceLabel;
18095
19135
  if (source.path !== void 0) {
18096
- const resolved = path6.resolve(source.path);
19136
+ const resolved = path8.resolve(source.path);
18097
19137
  try {
18098
- bytes = fs5.readFileSync(resolved);
19138
+ bytes = fs7.readFileSync(resolved);
18099
19139
  } catch (err) {
18100
19140
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err.message}` }) }] };
18101
19141
  }
@@ -18288,7 +19328,7 @@ var init_tools = __esm({
18288
19328
  this.instanceManager.attachInstanceId(record, connected.instanceId);
18289
19329
  return wrapToolJsonText({ ...this.managedStatus(record), instance_id: connected.instanceId, connected: true });
18290
19330
  }
18291
- await new Promise((resolve6) => setTimeout(resolve6, 500));
19331
+ await new Promise((resolve7) => setTimeout(resolve7, 500));
18292
19332
  }
18293
19333
  }
18294
19334
  return wrapToolJsonText({ ...this.managedStatus(record), connected: false });
@@ -18320,17 +19360,17 @@ var init_tools = __esm({
18320
19360
  const rawSnapshotBase64 = mutable.raw_snapshot_base64;
18321
19361
  if (typeof rawSnapshotBase64 === "string") {
18322
19362
  if (typeof outputPath === "string" && outputPath !== "") {
18323
- const resolvedOutputPath = path6.resolve(outputPath);
18324
- fs5.mkdirSync(path6.dirname(resolvedOutputPath), { recursive: true });
18325
- fs5.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
19363
+ const resolvedOutputPath = path8.resolve(outputPath);
19364
+ fs7.mkdirSync(path8.dirname(resolvedOutputPath), { recursive: true });
19365
+ fs7.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
18326
19366
  mutable.output_path = resolvedOutputPath;
18327
19367
  }
18328
19368
  delete mutable.raw_snapshot_base64;
18329
19369
  }
18330
19370
  if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
18331
- const resolvedSummaryPath = path6.resolve(summaryOutputPath);
18332
- fs5.mkdirSync(path6.dirname(resolvedSummaryPath), { recursive: true });
18333
- fs5.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
19371
+ const resolvedSummaryPath = path8.resolve(summaryOutputPath);
19372
+ fs7.mkdirSync(path8.dirname(resolvedSummaryPath), { recursive: true });
19373
+ fs7.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
18334
19374
  mutable.summary_output_path = resolvedSummaryPath;
18335
19375
  }
18336
19376
  const baselineCapture = loadMicroProfilerBaseline(request.baseline, request.baseline_path);
@@ -18363,12 +19403,11 @@ var init_proxy_bridge_service = __esm({
18363
19403
  ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
18364
19404
  primaryBaseUrl;
18365
19405
  proxyInstanceId;
18366
- proxyRequestTimeout = 3e4;
18367
19406
  cachedInstances = [];
18368
19407
  refreshTimer;
18369
19408
  static REFRESH_INTERVAL_MS = 1e3;
18370
19409
  constructor(primaryBaseUrl) {
18371
- super();
19410
+ super("");
18372
19411
  this.primaryBaseUrl = primaryBaseUrl;
18373
19412
  this.proxyInstanceId = randomUUID3();
18374
19413
  this.refreshInstances();
@@ -18399,7 +19438,7 @@ var init_proxy_bridge_service = __esm({
18399
19438
  }
18400
19439
  async sendRequest(endpoint, data, targetInstanceId, targetRole) {
18401
19440
  const controller = new AbortController();
18402
- const timeoutId = setTimeout(() => controller.abort(), this.proxyRequestTimeout);
19441
+ const timeoutId = setTimeout(() => controller.abort(), resolveRequestTimeout(endpoint, this.requestTimeout));
18403
19442
  try {
18404
19443
  const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
18405
19444
  method: "POST",
@@ -18415,8 +19454,11 @@ var init_proxy_bridge_service = __esm({
18415
19454
  });
18416
19455
  clearTimeout(timeoutId);
18417
19456
  if (!response.ok) {
18418
- const body = await response.text();
18419
- throw new Error(`Proxy request failed (${response.status}): ${body}`);
19457
+ const body = await response.json().catch(() => void 0);
19458
+ if (body?.outcome === "unknown" && body.requestId) {
19459
+ throw new RequestOutcomeUnknownError(body.requestId, endpoint, resolveRequestTimeout(endpoint, this.requestTimeout));
19460
+ }
19461
+ throw new Error(`Proxy request failed (${response.status}): ${body?.error ?? "Unknown error"}`);
18420
19462
  }
18421
19463
  const result = await response.json();
18422
19464
  if (result.error) {
@@ -18431,6 +19473,26 @@ var init_proxy_bridge_service = __esm({
18431
19473
  throw err;
18432
19474
  }
18433
19475
  }
19476
+ async lookupRequestStatus(requestId) {
19477
+ try {
19478
+ const response = await fetch(`${this.primaryBaseUrl}/request/${encodeURIComponent(requestId)}/status`);
19479
+ if (response.status === 404)
19480
+ return void 0;
19481
+ if (!response.ok)
19482
+ throw new Error(`Request status lookup failed (${response.status})`);
19483
+ return response.json();
19484
+ } catch {
19485
+ return void 0;
19486
+ }
19487
+ }
19488
+ async requestCancellation(requestId) {
19489
+ const response = await fetch(`${this.primaryBaseUrl}/cancel`, {
19490
+ method: "POST",
19491
+ headers: { "Content-Type": "application/json" },
19492
+ body: JSON.stringify({ requestId })
19493
+ });
19494
+ return response.ok;
19495
+ }
18434
19496
  cleanupOldRequests() {
18435
19497
  }
18436
19498
  clearAllPendingRequests() {
@@ -18662,6 +19724,12 @@ var init_setup_registry = __esm({
18662
19724
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
18663
19725
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18664
19726
  import { CallToolRequestSchema as CallToolRequestSchema2, ErrorCode as ErrorCode2, ListToolsRequestSchema as ListToolsRequestSchema2, ListResourcesRequestSchema as ListResourcesRequestSchema2, ListResourceTemplatesRequestSchema as ListResourceTemplatesRequestSchema2, ReadResourceRequestSchema as ReadResourceRequestSchema2, SubscribeRequestSchema as SubscribeRequestSchema2, UnsubscribeRequestSchema as UnsubscribeRequestSchema2, McpError as McpError2 } from "@modelcontextprotocol/sdk/types.js";
19727
+ function resolveBridgeHost(host = process.env.ROBLOX_STUDIO_HOST) {
19728
+ return host?.trim() || "127.0.0.1";
19729
+ }
19730
+ function isLoopbackHost(host) {
19731
+ return host === "127.0.0.1" || host === "::1" || host === "localhost";
19732
+ }
18665
19733
  function shouldUseLazyToolLoading(value) {
18666
19734
  const flag = (value ?? "").trim().toLowerCase();
18667
19735
  return !(flag === "0" || flag === "false" || flag === "off");
@@ -18696,6 +19764,7 @@ var init_server = __esm({
18696
19764
  init_structured_output();
18697
19765
  init_server_instructions();
18698
19766
  init_resources();
19767
+ init_capability_policy();
18699
19768
  BloxForgeServer = class {
18700
19769
  server;
18701
19770
  tools;
@@ -18712,8 +19781,10 @@ var init_server = __esm({
18712
19781
  lazyTools;
18713
19782
  activeToolNames;
18714
19783
  registry;
19784
+ stdioCapabilities;
18715
19785
  constructor(config) {
18716
19786
  this.config = config;
19787
+ this.stdioCapabilities = parseCapabilities(process.env.BLOXFORGE_STDIO_CAPABILITIES);
18717
19788
  this.allowedToolNames = new Set(config.tools.map((t) => t.name));
18718
19789
  this.lazyTools = shouldUseLazyToolLoading(process.env.ROBLOX_MCP_LAZY_TOOLS);
18719
19790
  this.activeToolNames = new Set(CORE_TOOLS);
@@ -18731,7 +19802,7 @@ var init_server = __esm({
18731
19802
  },
18732
19803
  instructions: SERVER_INSTRUCTIONS
18733
19804
  });
18734
- this.bridge = new BridgeService();
19805
+ this.bridge = new BridgeService("");
18735
19806
  this.tools = new RobloxStudioTools(this.bridge);
18736
19807
  this.registry = new ToolRegistry();
18737
19808
  registerContractedTools(this.registry, this.tools);
@@ -18778,6 +19849,11 @@ var init_server = __esm({
18778
19849
  if (!this.allowedToolNames.has(name)) {
18779
19850
  throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
18780
19851
  }
19852
+ const definition = this.config.tools.find((tool) => tool.name === name);
19853
+ const capability = definition ? requiredCapability(name, definition.category) : void 0;
19854
+ if (capability && this.stdioCapabilities && !this.stdioCapabilities.has(capability)) {
19855
+ throw new McpError2(ErrorCode2.InvalidRequest, `Capability required: ${capability}`);
19856
+ }
18781
19857
  try {
18782
19858
  const registryResult = await this.registry.callTool(name, this.tools, args ?? {});
18783
19859
  if (registryResult !== null && registryResult !== void 0) {
@@ -18862,7 +19938,10 @@ var init_server = __esm({
18862
19938
  }
18863
19939
  async run() {
18864
19940
  const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
18865
- const host = process.env.ROBLOX_STUDIO_HOST || "0.0.0.0";
19941
+ const host = resolveBridgeHost();
19942
+ if (!isLoopbackHost(host)) {
19943
+ console.error(`[security] Bridge is listening on ${host}. This explicit non-loopback mode exposes unauthenticated local-control endpoints.`);
19944
+ }
18866
19945
  let bridgeMode = "primary";
18867
19946
  let httpHandle;
18868
19947
  let primaryApp;
@@ -18872,6 +19951,7 @@ var init_server = __esm({
18872
19951
  primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, this.registry);
18873
19952
  const result = await listenWithRetry(primaryApp, host, basePort, 1);
18874
19953
  httpHandle = result.server;
19954
+ this.bridge.enableJournal();
18875
19955
  boundPort = result.port;
18876
19956
  console.error(`HTTP server listening on ${host}:${boundPort} for Studio plugin (primary mode)`);
18877
19957
  console.error(`Streamable HTTP MCP endpoint: http://localhost:${boundPort}/mcp`);
@@ -18884,12 +19964,13 @@ var init_server = __esm({
18884
19964
  console.error(`Port ${basePort} in use - entering proxy mode (forwarding to localhost:${basePort})`);
18885
19965
  const promotionIntervalMs = parseInt(process.env.ROBLOX_STUDIO_PROXY_PROMOTION_INTERVAL_MS || "5000");
18886
19966
  promotionInterval = setInterval(async () => {
18887
- const candidateBridge = new BridgeService();
19967
+ const candidateBridge = new BridgeService("");
18888
19968
  const candidateTools = new RobloxStudioTools(candidateBridge);
18889
19969
  const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config);
18890
19970
  try {
18891
19971
  const result = await listenWithRetry(candidateApp, host, basePort, 1);
18892
19972
  const oldBridge = this.bridge;
19973
+ candidateBridge.enableJournal();
18893
19974
  this.bridge = candidateBridge;
18894
19975
  this.tools = candidateTools;
18895
19976
  if (oldBridge instanceof ProxyBridgeService) {
@@ -18951,7 +20032,18 @@ var init_server = __esm({
18951
20032
  this.bridge.cleanupOldRequests();
18952
20033
  this.bridge.cleanupStaleInstances();
18953
20034
  }, 5e3);
20035
+ let shuttingDown = false;
20036
+ const closeHttp = (handle) => new Promise((resolve7) => {
20037
+ if (!handle) {
20038
+ resolve7();
20039
+ return;
20040
+ }
20041
+ handle.close(() => resolve7());
20042
+ });
18954
20043
  const shutdown = async () => {
20044
+ if (shuttingDown)
20045
+ return;
20046
+ shuttingDown = true;
18955
20047
  console.error("Shutting down MCP server...");
18956
20048
  clearInterval(activityInterval);
18957
20049
  clearInterval(cleanupInterval);
@@ -18960,12 +20052,14 @@ var init_server = __esm({
18960
20052
  if (this.bridge instanceof ProxyBridgeService) {
18961
20053
  this.bridge.stop();
18962
20054
  }
20055
+ if (primaryApp)
20056
+ primaryApp.setMCPServerActive(false);
20057
+ if (legacyApp)
20058
+ legacyApp.setMCPServerActive(false);
20059
+ this.bridge.clearAllPendingRequests();
18963
20060
  await this.server.close().catch(() => {
18964
20061
  });
18965
- if (httpHandle)
18966
- httpHandle.close();
18967
- if (legacyHandle)
18968
- legacyHandle.close();
20062
+ await Promise.all([closeHttp(httpHandle), closeHttp(legacyHandle)]);
18969
20063
  process.exit(0);
18970
20064
  };
18971
20065
  process.on("SIGTERM", shutdown);
@@ -18978,16 +20072,23 @@ var init_server = __esm({
18978
20072
  }
18979
20073
  });
18980
20074
 
20075
+ // ../core/dist/protocol-manifest.js
20076
+ var init_protocol_manifest = __esm({
20077
+ "../core/dist/protocol-manifest.js"() {
20078
+ "use strict";
20079
+ }
20080
+ });
20081
+
18981
20082
  // ../core/dist/install-plugin-helpers.js
18982
- import { existsSync as existsSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
20083
+ import { existsSync as existsSync7, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
18983
20084
  import { execSync } from "child_process";
18984
- import { join as join6 } from "path";
18985
- import { homedir as homedir5 } from "os";
20085
+ import { join as join8 } from "path";
20086
+ import { homedir as homedir6 } from "os";
18986
20087
  function isWSL() {
18987
20088
  if (process.platform !== "linux")
18988
20089
  return false;
18989
20090
  try {
18990
- const v = readFileSync6("/proc/version", "utf8");
20091
+ const v = readFileSync8("/proc/version", "utf8");
18991
20092
  return /microsoft|wsl/i.test(v);
18992
20093
  } catch {
18993
20094
  return false;
@@ -19005,7 +20106,7 @@ function getWindowsUserPluginsDir() {
19005
20106
  }).toString().trim();
19006
20107
  if (!linuxPath)
19007
20108
  return null;
19008
- return join6(linuxPath, "Roblox", "Plugins");
20109
+ return join8(linuxPath, "Roblox", "Plugins");
19009
20110
  } catch {
19010
20111
  return null;
19011
20112
  }
@@ -19014,7 +20115,7 @@ function getPluginsFolder() {
19014
20115
  if (process.env.MCP_PLUGINS_DIR)
19015
20116
  return process.env.MCP_PLUGINS_DIR;
19016
20117
  if (process.platform === "win32") {
19017
- return join6(process.env.LOCALAPPDATA || join6(homedir5(), "AppData", "Local"), "Roblox", "Plugins");
20118
+ return join8(process.env.LOCALAPPDATA || join8(homedir6(), "AppData", "Local"), "Roblox", "Plugins");
19018
20119
  }
19019
20120
  if (isWSL()) {
19020
20121
  const win = getWindowsUserPluginsDir();
@@ -19022,11 +20123,11 @@ function getPluginsFolder() {
19022
20123
  return win;
19023
20124
  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.");
19024
20125
  }
19025
- return join6(homedir5(), "Documents", "Roblox", "Plugins");
20126
+ return join8(homedir6(), "Documents", "Roblox", "Plugins");
19026
20127
  }
19027
20128
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
19028
- const otherDest = join6(pluginsFolder, otherAssetName);
19029
- if (!existsSync4(otherDest))
20129
+ const otherDest = join8(pluginsFolder, otherAssetName);
20130
+ if (!existsSync7(otherDest))
19030
20131
  return;
19031
20132
  if (replace) {
19032
20133
  try {
@@ -19054,6 +20155,7 @@ var init_doctor = __esm({
19054
20155
  "../core/dist/doctor.js"() {
19055
20156
  "use strict";
19056
20157
  init_install_plugin_helpers();
20158
+ init_bridge_service();
19057
20159
  }
19058
20160
  });
19059
20161
 
@@ -19062,8 +20164,11 @@ var init_dist = __esm({
19062
20164
  "../core/dist/index.js"() {
19063
20165
  "use strict";
19064
20166
  init_server();
20167
+ init_server();
19065
20168
  init_http_server();
19066
20169
  init_bridge_service();
20170
+ init_protocol_manifest();
20171
+ init_quality_tools();
19067
20172
  init_tools();
19068
20173
  init_studio_client();
19069
20174
  init_definitions();
@@ -19081,13 +20186,13 @@ __export(install_plugin_exports, {
19081
20186
  installBundledPlugin: () => installBundledPlugin,
19082
20187
  installPlugin: () => installPlugin
19083
20188
  });
19084
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
19085
- import { dirname as dirname6, join as join7 } from "path";
20189
+ import { copyFileSync as copyFileSync3, createWriteStream, existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync9, unlinkSync as unlinkSync3 } from "fs";
20190
+ import { dirname as dirname8, join as join9 } from "path";
19086
20191
  import { fileURLToPath } from "url";
19087
20192
  import { get } from "https";
19088
20193
  function httpsGet(url) {
19089
- return new Promise((resolve6, reject) => {
19090
- const req = get(url, { headers: { "User-Agent": "bloxforge-inspector" } }, resolve6);
20194
+ return new Promise((resolve7, reject) => {
20195
+ const req = get(url, { headers: { "User-Agent": "bloxforge-inspector" } }, resolve7);
19091
20196
  req.on("error", reject);
19092
20197
  req.setTimeout(TIMEOUT_MS, () => {
19093
20198
  req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
@@ -19105,7 +20210,7 @@ async function download(url, dest, redirects = 0) {
19105
20210
  if (res.statusCode !== 200) {
19106
20211
  throw new Error(`Download failed: HTTP ${res.statusCode}`);
19107
20212
  }
19108
- return new Promise((resolve6, reject) => {
20213
+ return new Promise((resolve7, reject) => {
19109
20214
  const file = createWriteStream(dest);
19110
20215
  const cleanup = (err) => {
19111
20216
  file.close(() => {
@@ -19119,7 +20224,7 @@ async function download(url, dest, redirects = 0) {
19119
20224
  res.pipe(file);
19120
20225
  file.on("finish", () => {
19121
20226
  file.close();
19122
- resolve6();
20227
+ resolve7();
19123
20228
  });
19124
20229
  file.on("error", cleanup);
19125
20230
  res.on("error", cleanup);
@@ -19142,8 +20247,8 @@ function prepareInstall({
19142
20247
  warn
19143
20248
  }) {
19144
20249
  const pluginsFolder = getPluginsFolder();
19145
- if (!existsSync5(pluginsFolder)) {
19146
- mkdirSync7(pluginsFolder, { recursive: true });
20250
+ if (!existsSync8(pluginsFolder)) {
20251
+ mkdirSync8(pluginsFolder, { recursive: true });
19147
20252
  }
19148
20253
  handleVariantConflict({
19149
20254
  pluginsFolder,
@@ -19155,23 +20260,23 @@ function prepareInstall({
19155
20260
  return pluginsFolder;
19156
20261
  }
19157
20262
  function bundledAssetPath() {
19158
- const currentDir = dirname6(fileURLToPath(import.meta.url));
20263
+ const currentDir = dirname8(fileURLToPath(import.meta.url));
19159
20264
  const candidates = [
19160
- join7(currentDir, "..", "studio-plugin", ASSET_NAME),
19161
- join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
20265
+ join9(currentDir, "..", "studio-plugin", ASSET_NAME),
20266
+ join9(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
19162
20267
  ];
19163
- return candidates.find((candidate) => existsSync5(candidate)) ?? null;
20268
+ return candidates.find((candidate) => existsSync8(candidate)) ?? null;
19164
20269
  }
19165
20270
  function packageVersion() {
19166
- const currentDir = dirname6(fileURLToPath(import.meta.url));
19167
- const pkg = JSON.parse(readFileSync7(join7(currentDir, "..", "package.json"), "utf8"));
20271
+ const currentDir = dirname8(fileURLToPath(import.meta.url));
20272
+ const pkg = JSON.parse(readFileSync9(join9(currentDir, "..", "package.json"), "utf8"));
19168
20273
  if (!pkg.version) {
19169
20274
  throw new Error("Package version not found");
19170
20275
  }
19171
20276
  return pkg.version;
19172
20277
  }
19173
20278
  function bundledPluginVersion(source) {
19174
- const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
20279
+ const match = readFileSync9(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
19175
20280
  return match ? match[1] : null;
19176
20281
  }
19177
20282
  function assertBundledPluginVersion(source) {
@@ -19184,9 +20289,9 @@ function assertBundledPluginVersion(source) {
19184
20289
  }
19185
20290
  }
19186
20291
  function filesMatch(a, b) {
19187
- if (!existsSync5(b)) return false;
19188
- const aBytes = readFileSync7(a);
19189
- const bBytes = readFileSync7(b);
20292
+ if (!existsSync8(b)) return false;
20293
+ const aBytes = readFileSync9(a);
20294
+ const bBytes = readFileSync9(b);
19190
20295
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
19191
20296
  }
19192
20297
  async function installBundledPlugin(options = {}) {
@@ -19199,9 +20304,9 @@ async function installBundledPlugin(options = {}) {
19199
20304
  }
19200
20305
  assertBundledPluginVersion(source);
19201
20306
  const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
19202
- const dest = join7(pluginsFolder, ASSET_NAME);
20307
+ const dest = join9(pluginsFolder, ASSET_NAME);
19203
20308
  if (filesMatch(source, dest)) return;
19204
- copyFileSync2(source, dest);
20309
+ copyFileSync3(source, dest);
19205
20310
  log(`Installed ${ASSET_NAME} to ${dest}`);
19206
20311
  }
19207
20312
  async function installPlugin(options = {}) {
@@ -19212,12 +20317,12 @@ async function installPlugin(options = {}) {
19212
20317
  const bundled = bundledAssetPath();
19213
20318
  if (bundled) {
19214
20319
  assertBundledPluginVersion(bundled);
19215
- const dest2 = join7(pluginsFolder, ASSET_NAME);
20320
+ const dest2 = join9(pluginsFolder, ASSET_NAME);
19216
20321
  if (filesMatch(bundled, dest2)) {
19217
20322
  log(`${ASSET_NAME} already installed.`);
19218
20323
  return;
19219
20324
  }
19220
- copyFileSync2(bundled, dest2);
20325
+ copyFileSync3(bundled, dest2);
19221
20326
  log(`Installed bundled ${ASSET_NAME} to ${dest2}`);
19222
20327
  return;
19223
20328
  }
@@ -19227,7 +20332,7 @@ async function installPlugin(options = {}) {
19227
20332
  if (!asset) {
19228
20333
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
19229
20334
  }
19230
- const dest = join7(pluginsFolder, ASSET_NAME);
20335
+ const dest = join9(pluginsFolder, ASSET_NAME);
19231
20336
  log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
19232
20337
  await download(asset.browser_download_url, dest);
19233
20338
  log(`Installed to ${dest}`);
@@ -19248,7 +20353,16 @@ var init_install_plugin = __esm({
19248
20353
  // src/index.ts
19249
20354
  init_dist();
19250
20355
  import { createRequire } from "module";
19251
- if (process.argv.includes("--install-plugin")) {
20356
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
20357
+ console.log(`BloxForge Inspector MCP Server
20358
+
20359
+ Usage: npx @princeofscale/bloxforge-inspector [options]
20360
+
20361
+ Options:
20362
+ --install-plugin Install the read-only Studio plugin
20363
+ --auto-install-plugin Install the plugin without prompting
20364
+ --help, -h Show this help message`);
20365
+ } else if (process.argv.includes("--install-plugin")) {
19252
20366
  const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
19253
20367
  await installPlugin2().catch((err) => {
19254
20368
  console.error(err instanceof Error ? err.message : String(err));