@princeofscale/bloxforge 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 +1703 -402
- package/package.json +5 -2
- package/studio-plugin/MCPInspectorPlugin.rbxmx +460 -101
- package/studio-plugin/MCPPlugin.rbxmx +460 -101
- package/studio-plugin/package-lock.json +9 -9
- package/studio-plugin/src/modules/ClientBroker.ts +67 -14
- package/studio-plugin/src/modules/Communication.ts +177 -28
- package/studio-plugin/src/modules/LuauExec.ts +6 -3
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +31 -2
- package/studio-plugin/src/modules/State.ts +1 -1
- package/studio-plugin/src/modules/handlers/JobHandlers.ts +2 -1
- package/studio-plugin/src/types/index.d.ts +7 -0
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
|
-
|
|
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 path10 = defaultRequestJournalPath();
|
|
224
|
+
if (path10)
|
|
225
|
+
this.journal = new RequestJournal(path10);
|
|
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 path10 = defaultRequestJournalPath();
|
|
266
|
+
if (path10)
|
|
267
|
+
this.journal = new RequestJournal(path10);
|
|
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
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
if (!removed)
|
|
422
|
+
const instance = this.instances.get(pluginSessionId);
|
|
423
|
+
if (!instance)
|
|
221
424
|
return;
|
|
222
|
-
this.
|
|
425
|
+
this.instances.delete(pluginSessionId);
|
|
426
|
+
this.recentDisconnects.push({
|
|
223
427
|
pluginSessionId,
|
|
224
|
-
instanceId:
|
|
225
|
-
role:
|
|
428
|
+
instanceId: instance.instanceId,
|
|
429
|
+
role: instance.role,
|
|
226
430
|
reason,
|
|
227
431
|
disconnectedAt: Date.now(),
|
|
228
|
-
lastActivity:
|
|
432
|
+
lastActivity: instance.lastActivity
|
|
229
433
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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(
|
|
441
|
+
clearTimeout(request.timeoutId);
|
|
235
442
|
this.pendingRequests.delete(id);
|
|
236
|
-
|
|
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((
|
|
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
|
-
|
|
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:
|
|
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" });
|
|
786
|
+
}
|
|
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++;
|
|
494
804
|
}
|
|
495
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
|
|
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
|
|
890
|
-
return { kind: "node", path:
|
|
1325
|
+
const path10 = decodeURIComponent(segments.slice(1).join("/"));
|
|
1326
|
+
return { kind: "node", path: path10 };
|
|
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
|
-
|
|
1253
|
-
app.use(express.json({ limit:
|
|
1254
|
-
app.use(express.urlencoded({ limit:
|
|
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 &&
|
|
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 (
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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({
|
|
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 (
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
2322
|
+
if (typeof message.requestId !== "string")
|
|
1760
2323
|
return;
|
|
1761
|
-
|
|
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);
|
|
2336
|
+
return;
|
|
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(
|
|
2183
|
-
const trimmed =
|
|
2781
|
+
isProtectedPath(path10) {
|
|
2782
|
+
const trimmed = path10.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(
|
|
2197
|
-
const existing = this.backups.get(
|
|
2198
|
-
this.backups.set(
|
|
2199
|
-
path:
|
|
2795
|
+
backupScript(path10, source) {
|
|
2796
|
+
const existing = this.backups.get(path10);
|
|
2797
|
+
this.backups.set(path10, {
|
|
2798
|
+
path: path10,
|
|
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(
|
|
2211
|
-
return this.backups.get(
|
|
2809
|
+
getBackup(path10) {
|
|
2810
|
+
return this.backups.get(path10);
|
|
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(
|
|
7353
|
-
const safePath = luaString(
|
|
8052
|
+
function buildWorldSnapshotLuau(path10 = "game", level = "overview", topNPerClass = 12) {
|
|
8053
|
+
const safePath = luaString(path10);
|
|
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,
|
|
7462
|
-
const safePath = luaString(
|
|
8163
|
+
function buildSceneSearchLuau(query, path10 = "game", limit = 10) {
|
|
8164
|
+
const safePath = luaString(path10);
|
|
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(
|
|
7529
|
-
const safePath = luaString(
|
|
8232
|
+
function buildWorldFingerprintLuau(path10 = "game", maxNodes = 8e3) {
|
|
8233
|
+
const safePath = luaString(path10);
|
|
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(
|
|
8410
|
+
put(path10, fingerprint) {
|
|
7705
8411
|
const id = this.newId();
|
|
7706
|
-
this.snapshots.set(id, { id, path:
|
|
8412
|
+
this.snapshots.set(id, { id, path: path10, fingerprint, createdAt: Date.now() });
|
|
7707
8413
|
this.prune();
|
|
7708
8414
|
return id;
|
|
7709
8415
|
}
|
|
@@ -8697,8 +9403,8 @@ function crc32(buf) {
|
|
|
8697
9403
|
crc = CRC_TABLE[(crc ^ buf[i]) & 255] ^ crc >>> 8;
|
|
8698
9404
|
return (crc ^ 4294967295) >>> 0;
|
|
8699
9405
|
}
|
|
8700
|
-
function writeChunk(
|
|
8701
|
-
const typeBytes = Buffer.from(
|
|
9406
|
+
function writeChunk(type2, data) {
|
|
9407
|
+
const typeBytes = Buffer.from(type2, "ascii");
|
|
8702
9408
|
const length = Buffer.alloc(4);
|
|
8703
9409
|
length.writeUInt32BE(data.length);
|
|
8704
9410
|
const crcInput = Buffer.concat([typeBytes, data]);
|
|
@@ -8763,7 +9469,7 @@ function encodeImageFromRgbaResponse(response, format, quality) {
|
|
|
8763
9469
|
};
|
|
8764
9470
|
}
|
|
8765
9471
|
function sleep(ms) {
|
|
8766
|
-
return new Promise((
|
|
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(
|
|
9203
|
-
const code = buildWorldSnapshotLuau(
|
|
9908
|
+
async getWorldSnapshot(path10, level, topNPerClass, instance_id) {
|
|
9909
|
+
const code = buildWorldSnapshotLuau(path10 ?? "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,
|
|
9915
|
+
async sceneSearch(query, path10, 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,
|
|
9919
|
+
const response = await this.runtime.callSingle("/api/execute-luau", { code: buildSceneSearchLuau(query, path10 ?? "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(
|
|
9234
|
-
const response = await this.runtime.callSingle("/api/execute-luau", { code: buildWorldFingerprintLuau(
|
|
9939
|
+
async _captureFingerprint(path10, instance_id) {
|
|
9940
|
+
const response = await this.runtime.callSingle("/api/execute-luau", { code: buildWorldFingerprintLuau(path10) }, "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,
|
|
9248
|
-
const p =
|
|
9953
|
+
async getChangesSince(snapshotId, path10, instance_id) {
|
|
9954
|
+
const p = path10 ?? "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(
|
|
9413
|
-
const safePath = luaString(
|
|
10118
|
+
function buildSceneSummaryLuau(path10 = "game.Workspace", topN = 20) {
|
|
10119
|
+
const safePath = luaString(path10);
|
|
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(
|
|
9463
|
-
const response = await this.runtime.callSingle("/api/file-tree", { path:
|
|
10168
|
+
async getFileTree(path10 = "", instance_id) {
|
|
10169
|
+
const response = await this.runtime.callSingle("/api/file-tree", { path: path10 }, 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(
|
|
10223
|
+
async getProjectStructure(path10, maxDepth, scriptsOnly, instance_id) {
|
|
9518
10224
|
const response = await this.runtime.callSingle("/api/project-structure", {
|
|
9519
|
-
path:
|
|
10225
|
+
path: path10,
|
|
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
|
|
10574
|
-
import * as
|
|
10575
|
-
import * as
|
|
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 =
|
|
11333
|
+
let dir = path2.resolve(startDir);
|
|
10592
11334
|
let previous = "";
|
|
10593
11335
|
while (dir !== previous) {
|
|
10594
|
-
if (
|
|
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 =
|
|
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
|
|
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 =
|
|
11353
|
+
const resolved = path2.resolve(candidate);
|
|
10612
11354
|
try {
|
|
10613
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
10634
|
-
const
|
|
10635
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
10771
|
-
const dirPath =
|
|
10772
|
-
if (!
|
|
10773
|
-
|
|
10774
|
-
|
|
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 =
|
|
10787
|
-
const dirPath =
|
|
10788
|
-
if (!
|
|
10789
|
-
|
|
10790
|
-
|
|
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 =
|
|
10816
|
-
const dirPath =
|
|
10817
|
-
if (!
|
|
10818
|
-
|
|
10819
|
-
|
|
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 =
|
|
10834
|
-
if (!
|
|
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(
|
|
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 (
|
|
11593
|
+
if (fs2.existsSync(libPath)) {
|
|
10850
11594
|
const readDir = (dir) => {
|
|
10851
|
-
for (const entry of
|
|
11595
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
10852
11596
|
if (entry.isDirectory()) {
|
|
10853
|
-
readDir(
|
|
11597
|
+
readDir(path2.join(dir, entry.name));
|
|
10854
11598
|
} else if (entry.name.endsWith(".json")) {
|
|
10855
11599
|
try {
|
|
10856
|
-
const data = JSON.parse(
|
|
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 =
|
|
10882
|
-
if (!
|
|
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(
|
|
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 =
|
|
10929
|
-
if (!
|
|
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(
|
|
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 (!
|
|
11841
|
+
if (!fs2.existsSync(filePath))
|
|
11098
11842
|
throw new Error(`File not found: ${filePath}`);
|
|
11099
|
-
const fileContent =
|
|
11100
|
-
const fileName =
|
|
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 =
|
|
11893
|
+
const resolved = path2.resolve(outputPath);
|
|
11150
11894
|
try {
|
|
11151
|
-
|
|
11152
|
-
|
|
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 =
|
|
11920
|
+
const resolved = path2.resolve(source.path);
|
|
11177
11921
|
try {
|
|
11178
|
-
bytes =
|
|
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 =
|
|
11231
|
-
|
|
11232
|
-
const file =
|
|
11233
|
-
|
|
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
|
|
11744
|
-
import * as
|
|
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 =
|
|
12630
|
-
|
|
12631
|
-
|
|
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 = [
|
|
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 = [
|
|
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
|
|
14713
|
-
import * as
|
|
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
|
|
15510
|
+
return path4.join(dir, ".robloxsync.json");
|
|
14734
15511
|
}
|
|
14735
15512
|
_readManifest(dir) {
|
|
14736
15513
|
try {
|
|
14737
|
-
const raw =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
14775
|
-
const rel =
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
14803
|
-
|
|
14804
|
-
|
|
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((
|
|
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
|
|
15139
|
-
import * as
|
|
15140
|
-
import * as
|
|
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
|
-
|
|
15147
|
-
|
|
15148
|
-
if (process.platform === "darwin") {
|
|
15149
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
16033
|
+
const stat = fs5.statSync(lockDir);
|
|
15243
16034
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
15244
|
-
|
|
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
|
-
|
|
16050
|
+
fs5.rmSync(lockDir, { recursive: true, force: true });
|
|
15260
16051
|
}
|
|
15261
16052
|
}
|
|
15262
16053
|
ensureDir() {
|
|
15263
|
-
|
|
16054
|
+
fs5.mkdirSync(this.dir, { recursive: true });
|
|
15264
16055
|
}
|
|
15265
16056
|
recordPath(recordId) {
|
|
15266
|
-
return
|
|
16057
|
+
return path5.join(this.dir, `${recordId}.json`);
|
|
15267
16058
|
}
|
|
15268
16059
|
recordFilesUnlocked() {
|
|
15269
|
-
return
|
|
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(
|
|
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(
|
|
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 =
|
|
15299
|
-
const fd =
|
|
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
|
-
|
|
16092
|
+
fs5.writeFileSync(fd, `${JSON.stringify(record, null, 2)}
|
|
15302
16093
|
`, "utf8");
|
|
15303
|
-
|
|
16094
|
+
fs5.fsyncSync(fd);
|
|
15304
16095
|
} finally {
|
|
15305
|
-
|
|
16096
|
+
fs5.closeSync(fd);
|
|
15306
16097
|
}
|
|
15307
|
-
|
|
16098
|
+
fs5.renameSync(tmpPath, finalPath);
|
|
15308
16099
|
}
|
|
15309
16100
|
deleteRecordUnlocked(recordId) {
|
|
15310
|
-
|
|
16101
|
+
fs5.rmSync(this.recordPath(recordId), { force: true });
|
|
15311
16102
|
}
|
|
15312
16103
|
appendEventUnlocked(event, now) {
|
|
15313
|
-
const file =
|
|
15314
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
16142
|
+
parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
15352
16143
|
} catch {
|
|
15353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
15430
|
-
import * as
|
|
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(
|
|
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() &&
|
|
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:
|
|
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() || !
|
|
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
|
|
16271
|
+
return path6.dirname(realpathSync(entrypoint));
|
|
15481
16272
|
} catch {
|
|
15482
|
-
return
|
|
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
|
-
|
|
15490
|
-
|
|
16280
|
+
path6.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
|
|
16281
|
+
path6.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
|
|
15491
16282
|
] : [],
|
|
15492
|
-
|
|
15493
|
-
|
|
15494
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
16324
|
+
mkdirSync6(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
15534
16325
|
sweepStaleBaseplateFiles();
|
|
15535
|
-
const file =
|
|
15536
|
-
|
|
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 =
|
|
15541
|
-
return
|
|
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 ?
|
|
15574
|
-
if (!
|
|
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) =>
|
|
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
|
|
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}:${
|
|
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((
|
|
16455
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
15665
16456
|
}
|
|
15666
16457
|
function basenameAny(filePath) {
|
|
15667
|
-
return
|
|
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 =
|
|
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() &&
|
|
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 ?
|
|
15930
|
-
const exePath = record.exe ?
|
|
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(
|
|
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
|
|
16138
|
-
import * as
|
|
16139
|
-
import * as
|
|
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 =
|
|
16200
|
-
const parsed = JSON.parse(
|
|
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: (
|
|
17381
|
+
backupScript: (path10, source) => this.safety.backupScript(path10, 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(
|
|
16608
|
-
return this.sceneReadTools.getFileTree(
|
|
17645
|
+
async getFileTree(path10 = "", instance_id) {
|
|
17646
|
+
return this.sceneReadTools.getFileTree(path10, 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(
|
|
16643
|
-
return this.sceneReadTools.getProjectStructure(
|
|
17680
|
+
async getProjectStructure(path10, maxDepth, scriptsOnly, instance_id) {
|
|
17681
|
+
return this.sceneReadTools.getProjectStructure(path10, 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 =
|
|
17865
|
+
let dir = path8.resolve(startDir);
|
|
16828
17866
|
let previous = "";
|
|
16829
17867
|
while (dir !== previous) {
|
|
16830
|
-
if (
|
|
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 =
|
|
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
|
|
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 =
|
|
17886
|
+
const resolved = path8.resolve(candidate);
|
|
16849
17887
|
try {
|
|
16850
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
16872
|
-
const
|
|
16873
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
16915
|
-
const dirPath =
|
|
16916
|
-
if (!
|
|
16917
|
-
|
|
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
|
-
|
|
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 =
|
|
17017
|
-
const dirPath =
|
|
17018
|
-
if (!
|
|
17019
|
-
|
|
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
|
-
|
|
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 =
|
|
17076
|
-
const dirPath =
|
|
17077
|
-
if (!
|
|
17078
|
-
|
|
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
|
-
|
|
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 =
|
|
17105
|
-
if (!
|
|
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(
|
|
18148
|
+
resolved = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
17109
18149
|
} else if (buildData.id && !buildData.parts) {
|
|
17110
|
-
const filePath =
|
|
17111
|
-
if (!
|
|
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(
|
|
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 =
|
|
17138
|
-
if (!
|
|
18177
|
+
const dirPath = path8.join(libraryPath, s);
|
|
18178
|
+
if (!fs7.existsSync(dirPath))
|
|
17139
18179
|
continue;
|
|
17140
|
-
const files =
|
|
18180
|
+
const files = fs7.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
|
|
17141
18181
|
for (const file of files) {
|
|
17142
18182
|
try {
|
|
17143
|
-
const content =
|
|
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 =
|
|
17183
|
-
if (!
|
|
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(
|
|
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 =
|
|
17270
|
-
if (!
|
|
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 =
|
|
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(
|
|
17589
|
-
if (!
|
|
18628
|
+
async audioPlaySound(path10, instance_id) {
|
|
18629
|
+
if (!path10)
|
|
17590
18630
|
throw new Error("path is required for audio_play_sound");
|
|
17591
|
-
return this._runGeneratedLuau(buildPlaySoundLuau({ path:
|
|
18631
|
+
return this._runGeneratedLuau(buildPlaySoundLuau({ path: path10 }), 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 =
|
|
17699
|
-
filePath =
|
|
17700
|
-
|
|
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 (!
|
|
18744
|
+
if (!fs7.existsSync(options.source))
|
|
17705
18745
|
throw new Error(`File not found: ${options.source}`);
|
|
17706
18746
|
filePath = options.source;
|
|
17707
|
-
bytes =
|
|
18747
|
+
bytes = fs7.statSync(filePath).size;
|
|
17708
18748
|
}
|
|
17709
|
-
const sha256 = crypto.createHash("sha256").update(
|
|
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 ??
|
|
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
|
-
|
|
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 =
|
|
17780
|
-
|
|
17781
|
-
const file =
|
|
17782
|
-
|
|
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 (!
|
|
18888
|
+
if (!fs7.existsSync(filePath)) {
|
|
17849
18889
|
throw new Error(`File not found: ${filePath}`);
|
|
17850
18890
|
}
|
|
17851
|
-
const fileContent =
|
|
17852
|
-
const fileName =
|
|
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
|
|
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(
|
|
17989
|
-
Promise.resolve(this.getOperationHistory(25)).then(
|
|
17990
|
-
Promise.resolve(this.getConnectedInstances()).then(
|
|
17991
|
-
Promise.resolve(this.listEpisodes()).then(
|
|
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(
|
|
18012
|
-
return this.worldTools.getWorldSnapshot(
|
|
19051
|
+
async getWorldSnapshot(path10, level, topNPerClass, instance_id) {
|
|
19052
|
+
return this.worldTools.getWorldSnapshot(path10, level, topNPerClass, instance_id);
|
|
18013
19053
|
}
|
|
18014
|
-
async sceneSearch(query,
|
|
18015
|
-
return this.worldTools.sceneSearch(query,
|
|
19054
|
+
async sceneSearch(query, path10, limit, instance_id) {
|
|
19055
|
+
return this.worldTools.sceneSearch(query, path10, 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,
|
|
18021
|
-
return this.worldTools.getChangesSince(snapshotId,
|
|
19060
|
+
async getChangesSince(snapshotId, path10, instance_id) {
|
|
19061
|
+
return this.worldTools.getChangesSince(snapshotId, path10, 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 =
|
|
19100
|
+
const resolved = path8.resolve(outputPath);
|
|
18061
19101
|
try {
|
|
18062
|
-
|
|
18063
|
-
|
|
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 =
|
|
19136
|
+
const resolved = path8.resolve(source.path);
|
|
18097
19137
|
try {
|
|
18098
|
-
bytes =
|
|
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((
|
|
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 =
|
|
18324
|
-
|
|
18325
|
-
|
|
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 =
|
|
18332
|
-
|
|
18333
|
-
|
|
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.
|
|
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.
|
|
18419
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
20083
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
18983
20084
|
import { execSync } from "child_process";
|
|
18984
|
-
import { join as
|
|
18985
|
-
import { homedir as
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
19029
|
-
if (!
|
|
20129
|
+
const otherDest = join8(pluginsFolder, otherAssetName);
|
|
20130
|
+
if (!existsSync7(otherDest))
|
|
19030
20131
|
return;
|
|
19031
20132
|
if (replace) {
|
|
19032
20133
|
try {
|
|
@@ -19050,20 +20151,43 @@ var init_install_plugin_helpers = __esm({
|
|
|
19050
20151
|
});
|
|
19051
20152
|
|
|
19052
20153
|
// ../core/dist/doctor.js
|
|
19053
|
-
import * as
|
|
19054
|
-
import * as
|
|
20154
|
+
import * as fs8 from "fs";
|
|
20155
|
+
import * as path9 from "path";
|
|
20156
|
+
import * as os7 from "os";
|
|
19055
20157
|
function checkNodeVersion(version) {
|
|
19056
20158
|
const major = parseInt(version.replace(/^v/, "").split(".")[0] ?? "0", 10);
|
|
19057
20159
|
if (Number.isNaN(major) || major < 18) {
|
|
19058
|
-
return {
|
|
20160
|
+
return {
|
|
20161
|
+
name: "Node version",
|
|
20162
|
+
status: "fail",
|
|
20163
|
+
detail: `${version} \u2014 Node 18+ is required.`,
|
|
20164
|
+
actionable: {
|
|
20165
|
+
fix: "Upgrade Node.js to version 18 or newer.",
|
|
20166
|
+
verify: 'Run "node -v" to verify your version, then run "npx @princeofscale/bloxforge verify".'
|
|
20167
|
+
}
|
|
20168
|
+
};
|
|
19059
20169
|
}
|
|
19060
20170
|
return { name: "Node version", status: "ok", detail: version };
|
|
19061
20171
|
}
|
|
19062
20172
|
function formatDoctorReport(checks) {
|
|
19063
|
-
const lines =
|
|
20173
|
+
const lines = [];
|
|
20174
|
+
for (const c of checks) {
|
|
20175
|
+
lines.push(` ${SYMBOL[c.status]} ${c.name}: ${c.detail}`);
|
|
20176
|
+
if (c.status !== "ok" && c.actionable) {
|
|
20177
|
+
lines.push(` Fix: ${c.actionable.fix}`);
|
|
20178
|
+
if (c.actionable.verify) {
|
|
20179
|
+
lines.push(` Verify: ${c.actionable.verify}`);
|
|
20180
|
+
}
|
|
20181
|
+
}
|
|
20182
|
+
}
|
|
19064
20183
|
const worst = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
|
|
19065
20184
|
const summary = worst === "ok" ? "All checks passed." : worst === "warn" ? "Some checks need attention (warnings)." : "Problems found \u2014 see failures above.";
|
|
19066
|
-
return ["bloxforge doctor", ...lines, "", summary].join("\n");
|
|
20185
|
+
return ["bloxforge doctor / verify", ...lines, "", summary].join("\n");
|
|
20186
|
+
}
|
|
20187
|
+
function fetchHealth(fetchImpl, port) {
|
|
20188
|
+
return fetchImpl(`http://localhost:${port}/health`, {
|
|
20189
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
20190
|
+
});
|
|
19067
20191
|
}
|
|
19068
20192
|
async function collectDoctorChecks(options = {}) {
|
|
19069
20193
|
const checks = [];
|
|
@@ -19076,42 +20200,94 @@ async function collectDoctorChecks(options = {}) {
|
|
|
19076
20200
|
try {
|
|
19077
20201
|
const folder = getPluginsFolder();
|
|
19078
20202
|
const variants = ["MCPPlugin.rbxmx", "MCPInspectorPlugin.rbxmx"];
|
|
19079
|
-
const found = variants.filter((v) =>
|
|
19080
|
-
checks.push(found.length > 0 ? { name: "Studio plugin installed", status: "ok", detail: `${found.join(", ")} in ${folder}` } : {
|
|
20203
|
+
const found = variants.filter((v) => fs8.existsSync(path9.join(folder, v)));
|
|
20204
|
+
checks.push(found.length > 0 ? { name: "Studio plugin installed", status: "ok", detail: `${found.join(", ")} in ${folder}` } : {
|
|
20205
|
+
name: "Studio plugin installed",
|
|
20206
|
+
status: "warn",
|
|
20207
|
+
detail: `none found in ${folder}. Run with --install-plugin.`,
|
|
20208
|
+
actionable: {
|
|
20209
|
+
fix: 'Run "npx @princeofscale/bloxforge --install-plugin" to install the plugin to your local Roblox directory.',
|
|
20210
|
+
verify: 'Run "npx @princeofscale/bloxforge verify" again to confirm installation.'
|
|
20211
|
+
}
|
|
20212
|
+
});
|
|
19081
20213
|
} catch (error) {
|
|
19082
|
-
checks.push({
|
|
20214
|
+
checks.push({
|
|
20215
|
+
name: "Studio plugin installed",
|
|
20216
|
+
status: "warn",
|
|
20217
|
+
detail: `could not resolve plugins folder: ${error instanceof Error ? error.message : String(error)}`,
|
|
20218
|
+
actionable: {
|
|
20219
|
+
fix: "Ensure your Roblox Studio installation is valid and accessible.",
|
|
20220
|
+
verify: "Check if Roblox Studio opens correctly, then try again."
|
|
20221
|
+
}
|
|
20222
|
+
});
|
|
19083
20223
|
}
|
|
19084
20224
|
const port = options.port ?? (process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741);
|
|
19085
20225
|
const doFetch = options.fetchImpl ?? fetch;
|
|
19086
20226
|
try {
|
|
19087
|
-
const res = await doFetch
|
|
20227
|
+
const res = await fetchHealth(doFetch, port);
|
|
19088
20228
|
if (res.ok) {
|
|
19089
20229
|
const health = await res.json();
|
|
19090
20230
|
checks.push({ name: "Local bridge running", status: "ok", detail: `responding on port ${port}` });
|
|
19091
|
-
checks.push(health.pluginConnected ? { name: "Studio reachable", status: "ok", detail: `${health.instanceCount ?? 0} place(s) connected` } : {
|
|
20231
|
+
checks.push(health.pluginConnected ? { name: "Studio reachable", status: "ok", detail: `${health.instanceCount ?? 0} place(s) connected` } : {
|
|
20232
|
+
name: "Studio reachable",
|
|
20233
|
+
status: "warn",
|
|
20234
|
+
detail: "bridge up but no Studio plugin connected.",
|
|
20235
|
+
actionable: {
|
|
20236
|
+
fix: 'Open Roblox Studio, open your place, and ensure "Allow HTTP Requests" is enabled in Game Settings -> Security. Play or Run the game.',
|
|
20237
|
+
verify: 'Ensure the BloxForge plugin shows "Connected" in Studio, then run verify again.'
|
|
20238
|
+
}
|
|
20239
|
+
});
|
|
19092
20240
|
checks.push({
|
|
19093
20241
|
name: "Lazy tool loading",
|
|
19094
20242
|
status: health.lazyTools === false ? "warn" : "ok",
|
|
19095
20243
|
detail: health.lazyTools === false ? "disabled via ROBLOX_MCP_LAZY_TOOLS opt-out; all schemas are advertised upfront" : `default path active (${health.activeToolCount ?? 0} active tools; loaded ${health.loadedToolsets?.join(", ") || "core"})`
|
|
19096
20244
|
});
|
|
19097
20245
|
const first = health.instances?.[0];
|
|
19098
|
-
|
|
20246
|
+
const versionInstance = health.instances?.find((instance) => instance.versionMismatch) ?? first;
|
|
20247
|
+
const protocolInstance = health.instances?.find((instance) => instance.protocolMismatch) ?? first;
|
|
20248
|
+
if (versionInstance) {
|
|
19099
20249
|
checks.push({
|
|
19100
20250
|
name: "Studio plugin version",
|
|
19101
|
-
status: health.versionMismatch ||
|
|
19102
|
-
detail: `plugin v${
|
|
20251
|
+
status: health.versionMismatch || versionInstance.versionMismatch ? "warn" : "ok",
|
|
20252
|
+
detail: `plugin v${versionInstance.pluginVersion ?? "unknown"} (${versionInstance.pluginVariant ?? "unknown"}), server v${health.serverVersion ?? health.version ?? options.version ?? "unknown"}`,
|
|
20253
|
+
actionable: health.versionMismatch || versionInstance.versionMismatch ? {
|
|
20254
|
+
fix: 'Run "npx @princeofscale/bloxforge --install-plugin" to synchronize the plugin version with your local server.',
|
|
20255
|
+
verify: 'Restart Roblox Studio, then run "npx @princeofscale/bloxforge verify" and confirm this check passes.'
|
|
20256
|
+
} : void 0
|
|
19103
20257
|
});
|
|
20258
|
+
}
|
|
20259
|
+
if (protocolInstance) {
|
|
19104
20260
|
checks.push({
|
|
19105
20261
|
name: "Protocol version",
|
|
19106
|
-
status: health.protocolMismatch ||
|
|
19107
|
-
detail: `plugin protocol ${
|
|
20262
|
+
status: health.protocolMismatch || protocolInstance.protocolMismatch ? "warn" : "ok",
|
|
20263
|
+
detail: `plugin protocol ${protocolInstance.pluginProtocolVersion ?? "unknown"}, server protocol ${protocolInstance.serverProtocolVersion ?? health.protocolVersion ?? "unknown"}`,
|
|
20264
|
+
actionable: health.protocolMismatch || protocolInstance.protocolMismatch ? {
|
|
20265
|
+
fix: "Your server and plugin are using incompatible communication protocols. Please update both to the latest versions.",
|
|
20266
|
+
verify: 'Restart Roblox Studio, then run "npx @princeofscale/bloxforge verify" and confirm both protocol versions match.'
|
|
20267
|
+
} : void 0
|
|
19108
20268
|
});
|
|
19109
20269
|
}
|
|
19110
20270
|
} else {
|
|
19111
|
-
checks.push({
|
|
20271
|
+
checks.push({
|
|
20272
|
+
name: "Local bridge running",
|
|
20273
|
+
status: "fail",
|
|
20274
|
+
detail: `port ${port} responded ${res.status}`,
|
|
20275
|
+
actionable: {
|
|
20276
|
+
fix: `Another application might be using port ${port}. Try running the server on a different port using --port <number>.`,
|
|
20277
|
+
verify: 'Run "npx @princeofscale/bloxforge verify --port <number>" on the new port and confirm this check passes.'
|
|
20278
|
+
}
|
|
20279
|
+
});
|
|
19112
20280
|
}
|
|
19113
20281
|
} catch {
|
|
19114
|
-
checks.push({
|
|
20282
|
+
checks.push({
|
|
20283
|
+
name: "Local bridge running",
|
|
20284
|
+
status: "warn",
|
|
20285
|
+
detail: `nothing responding on port ${port}. The bridge only runs while the MCP server is started.`,
|
|
20286
|
+
actionable: {
|
|
20287
|
+
fix: 'Start the BloxForge server in another terminal (e.g., via "npx @princeofscale/bloxforge") before running diagnostics.',
|
|
20288
|
+
verify: 'Keep the server running, then in a new terminal run "npx @princeofscale/bloxforge verify".'
|
|
20289
|
+
}
|
|
20290
|
+
});
|
|
19115
20291
|
}
|
|
19116
20292
|
return checks;
|
|
19117
20293
|
}
|
|
@@ -19120,12 +20296,85 @@ async function runDoctor(options = {}) {
|
|
|
19120
20296
|
console.log(formatDoctorReport(checks));
|
|
19121
20297
|
return checks.some((c) => c.status === "fail") ? 1 : 0;
|
|
19122
20298
|
}
|
|
19123
|
-
|
|
20299
|
+
async function generateDiagnosticReport(options = {}) {
|
|
20300
|
+
const checks = await collectDoctorChecks(options);
|
|
20301
|
+
const home = os7.homedir();
|
|
20302
|
+
const sanitize = (text) => {
|
|
20303
|
+
const escapedHome = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20304
|
+
return text.replace(new RegExp(escapedHome, "g"), "<home>");
|
|
20305
|
+
};
|
|
20306
|
+
const lines = [];
|
|
20307
|
+
lines.push("==================================================");
|
|
20308
|
+
lines.push(" BLOXFORGE DIAGNOSTIC REPORT ");
|
|
20309
|
+
lines.push("==================================================");
|
|
20310
|
+
lines.push("");
|
|
20311
|
+
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
20312
|
+
lines.push(`OS: ${process.platform} (${os7.type()} ${os7.release()} ${os7.arch()})`);
|
|
20313
|
+
lines.push(`Node Version: ${process.version}`);
|
|
20314
|
+
lines.push(`BloxForge Version: ${options.version ?? "unknown"}`);
|
|
20315
|
+
lines.push(`Protocol Version: ${MCP_PROTOCOL_VERSION}`);
|
|
20316
|
+
lines.push(`Selected Profile: ${process.env.BLOXFORGE_TOOL_PROFILE || "core"}`);
|
|
20317
|
+
lines.push("");
|
|
20318
|
+
lines.push("--- Doctor Results ---");
|
|
20319
|
+
for (const c of checks) {
|
|
20320
|
+
const statusSymbol = c.status === "ok" ? "[ OK ]" : c.status === "warn" ? "[WARN]" : "[FAIL]";
|
|
20321
|
+
lines.push(`${statusSymbol} ${c.name}: ${sanitize(c.detail)}`);
|
|
20322
|
+
}
|
|
20323
|
+
lines.push("");
|
|
20324
|
+
const port = options.port ?? (process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741);
|
|
20325
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
20326
|
+
try {
|
|
20327
|
+
const res = await fetchHealth(doFetch, port);
|
|
20328
|
+
if (res.ok) {
|
|
20329
|
+
const health = await res.json();
|
|
20330
|
+
lines.push("--- Running Server Status ---");
|
|
20331
|
+
lines.push(`Server Uptime: ${Math.round((health.uptime ?? 0) / 1e3)}s`);
|
|
20332
|
+
lines.push(`Lazy Tools Enabled: ${health.lazyTools ?? "unknown"}`);
|
|
20333
|
+
lines.push(`Active Tool Count: ${health.activeToolCount ?? "unknown"}`);
|
|
20334
|
+
lines.push(`Loaded Toolsets: ${health.loadedToolsets?.join(", ") ?? "none"}`);
|
|
20335
|
+
lines.push(`Connected Instances: ${health.instanceCount ?? 0}`);
|
|
20336
|
+
if (health.instances && health.instances.length > 0) {
|
|
20337
|
+
for (const inst of health.instances) {
|
|
20338
|
+
lines.push(` - Instance: ${inst.role} (variant: ${inst.pluginVariant ?? "unknown"}, version: ${inst.pluginVersion ?? "unknown"}, protocol: ${inst.pluginProtocolVersion ?? "unknown"})`);
|
|
20339
|
+
}
|
|
20340
|
+
}
|
|
20341
|
+
lines.push("");
|
|
20342
|
+
if (health.recentDisconnects && health.recentDisconnects.length > 0) {
|
|
20343
|
+
lines.push("--- Recent Disconnects ---");
|
|
20344
|
+
for (const disc of health.recentDisconnects) {
|
|
20345
|
+
lines.push(` - [${new Date(disc.disconnectedAt).toISOString()}] role: ${disc.role}, reason: ${disc.reason}`);
|
|
20346
|
+
}
|
|
20347
|
+
lines.push("");
|
|
20348
|
+
}
|
|
20349
|
+
if (health.session) {
|
|
20350
|
+
lines.push("--- Session Summary ---");
|
|
20351
|
+
lines.push(`Total Calls: ${health.session.totalCalls ?? 0}`);
|
|
20352
|
+
lines.push(`Failed Calls: ${health.session.failures ?? 0}`);
|
|
20353
|
+
if (health.session.byTool) {
|
|
20354
|
+
for (const stats of health.session.byTool) {
|
|
20355
|
+
lines.push(` - ${stats.toolName}: ${stats.calls} calls, ${stats.failures} failures, avg ${Math.round(stats.averageDurationMs ?? 0)}ms`);
|
|
20356
|
+
}
|
|
20357
|
+
}
|
|
20358
|
+
lines.push("");
|
|
20359
|
+
}
|
|
20360
|
+
}
|
|
20361
|
+
} catch {
|
|
20362
|
+
lines.push("--- Running Server Status ---");
|
|
20363
|
+
lines.push("MCP server is not currently running on this port.");
|
|
20364
|
+
lines.push("");
|
|
20365
|
+
}
|
|
20366
|
+
lines.push("==================================================");
|
|
20367
|
+
lines.push("End of Report");
|
|
20368
|
+
return lines.join("\n");
|
|
20369
|
+
}
|
|
20370
|
+
var SYMBOL, HEALTH_TIMEOUT_MS;
|
|
19124
20371
|
var init_doctor = __esm({
|
|
19125
20372
|
"../core/dist/doctor.js"() {
|
|
19126
20373
|
"use strict";
|
|
19127
20374
|
init_install_plugin_helpers();
|
|
20375
|
+
init_bridge_service();
|
|
19128
20376
|
SYMBOL = { ok: "\u2713", warn: "!", fail: "\u2717" };
|
|
20377
|
+
HEALTH_TIMEOUT_MS = 3e3;
|
|
19129
20378
|
}
|
|
19130
20379
|
});
|
|
19131
20380
|
|
|
@@ -19134,8 +20383,11 @@ var init_dist = __esm({
|
|
|
19134
20383
|
"../core/dist/index.js"() {
|
|
19135
20384
|
"use strict";
|
|
19136
20385
|
init_server();
|
|
20386
|
+
init_server();
|
|
19137
20387
|
init_http_server();
|
|
19138
20388
|
init_bridge_service();
|
|
20389
|
+
init_protocol_manifest();
|
|
20390
|
+
init_quality_tools();
|
|
19139
20391
|
init_tools();
|
|
19140
20392
|
init_studio_client();
|
|
19141
20393
|
init_definitions();
|
|
@@ -19153,13 +20405,13 @@ __export(install_plugin_exports, {
|
|
|
19153
20405
|
installBundledPlugin: () => installBundledPlugin,
|
|
19154
20406
|
installPlugin: () => installPlugin
|
|
19155
20407
|
});
|
|
19156
|
-
import { copyFileSync as
|
|
19157
|
-
import { dirname as
|
|
20408
|
+
import { copyFileSync as copyFileSync3, createWriteStream, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync9, unlinkSync as unlinkSync3 } from "fs";
|
|
20409
|
+
import { dirname as dirname8, join as join10 } from "path";
|
|
19158
20410
|
import { fileURLToPath } from "url";
|
|
19159
20411
|
import { get } from "https";
|
|
19160
20412
|
function httpsGet(url) {
|
|
19161
|
-
return new Promise((
|
|
19162
|
-
const req = get(url, { headers: { "User-Agent": "bloxforge" } },
|
|
20413
|
+
return new Promise((resolve7, reject) => {
|
|
20414
|
+
const req = get(url, { headers: { "User-Agent": "bloxforge" } }, resolve7);
|
|
19163
20415
|
req.on("error", reject);
|
|
19164
20416
|
req.setTimeout(TIMEOUT_MS, () => {
|
|
19165
20417
|
req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
|
|
@@ -19177,7 +20429,7 @@ async function download(url, dest, redirects = 0) {
|
|
|
19177
20429
|
if (res.statusCode !== 200) {
|
|
19178
20430
|
throw new Error(`Download failed: HTTP ${res.statusCode}`);
|
|
19179
20431
|
}
|
|
19180
|
-
return new Promise((
|
|
20432
|
+
return new Promise((resolve7, reject) => {
|
|
19181
20433
|
const file = createWriteStream(dest);
|
|
19182
20434
|
const cleanup = (err) => {
|
|
19183
20435
|
file.close(() => {
|
|
@@ -19191,7 +20443,7 @@ async function download(url, dest, redirects = 0) {
|
|
|
19191
20443
|
res.pipe(file);
|
|
19192
20444
|
file.on("finish", () => {
|
|
19193
20445
|
file.close();
|
|
19194
|
-
|
|
20446
|
+
resolve7();
|
|
19195
20447
|
});
|
|
19196
20448
|
file.on("error", cleanup);
|
|
19197
20449
|
res.on("error", cleanup);
|
|
@@ -19224,8 +20476,8 @@ function prepareInstall({
|
|
|
19224
20476
|
warn
|
|
19225
20477
|
}) {
|
|
19226
20478
|
const pluginsFolder = getPluginsFolder();
|
|
19227
|
-
if (!
|
|
19228
|
-
|
|
20479
|
+
if (!existsSync9(pluginsFolder)) {
|
|
20480
|
+
mkdirSync8(pluginsFolder, { recursive: true });
|
|
19229
20481
|
}
|
|
19230
20482
|
handleVariantConflict({
|
|
19231
20483
|
pluginsFolder,
|
|
@@ -19237,23 +20489,23 @@ function prepareInstall({
|
|
|
19237
20489
|
return pluginsFolder;
|
|
19238
20490
|
}
|
|
19239
20491
|
function bundledAssetPath() {
|
|
19240
|
-
const currentDir =
|
|
20492
|
+
const currentDir = dirname8(fileURLToPath(import.meta.url));
|
|
19241
20493
|
const candidates = [
|
|
19242
|
-
|
|
19243
|
-
|
|
20494
|
+
join10(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
20495
|
+
join10(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
19244
20496
|
];
|
|
19245
|
-
return candidates.find((candidate) =>
|
|
20497
|
+
return candidates.find((candidate) => existsSync9(candidate)) ?? null;
|
|
19246
20498
|
}
|
|
19247
20499
|
function packageVersion() {
|
|
19248
|
-
const currentDir =
|
|
19249
|
-
const pkg = JSON.parse(
|
|
20500
|
+
const currentDir = dirname8(fileURLToPath(import.meta.url));
|
|
20501
|
+
const pkg = JSON.parse(readFileSync9(join10(currentDir, "..", "package.json"), "utf8"));
|
|
19250
20502
|
if (!pkg.version) {
|
|
19251
20503
|
throw new Error("Package version not found");
|
|
19252
20504
|
}
|
|
19253
20505
|
return pkg.version;
|
|
19254
20506
|
}
|
|
19255
20507
|
function bundledPluginVersion(source) {
|
|
19256
|
-
const match =
|
|
20508
|
+
const match = readFileSync9(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
19257
20509
|
return match ? match[1] : null;
|
|
19258
20510
|
}
|
|
19259
20511
|
function assertBundledPluginVersion(source) {
|
|
@@ -19266,9 +20518,9 @@ function assertBundledPluginVersion(source) {
|
|
|
19266
20518
|
}
|
|
19267
20519
|
}
|
|
19268
20520
|
function filesMatch(a, b) {
|
|
19269
|
-
if (!
|
|
19270
|
-
const aBytes =
|
|
19271
|
-
const bBytes =
|
|
20521
|
+
if (!existsSync9(b)) return false;
|
|
20522
|
+
const aBytes = readFileSync9(a);
|
|
20523
|
+
const bBytes = readFileSync9(b);
|
|
19272
20524
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
19273
20525
|
}
|
|
19274
20526
|
async function installBundledPlugin(options = {}) {
|
|
@@ -19281,9 +20533,9 @@ async function installBundledPlugin(options = {}) {
|
|
|
19281
20533
|
}
|
|
19282
20534
|
assertBundledPluginVersion(source);
|
|
19283
20535
|
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
19284
|
-
const dest =
|
|
20536
|
+
const dest = join10(pluginsFolder, ASSET_NAME);
|
|
19285
20537
|
if (filesMatch(source, dest)) return;
|
|
19286
|
-
|
|
20538
|
+
copyFileSync3(source, dest);
|
|
19287
20539
|
log(`Installed ${ASSET_NAME} to ${dest}`);
|
|
19288
20540
|
}
|
|
19289
20541
|
async function installPlugin(options = {}) {
|
|
@@ -19295,23 +20547,23 @@ async function installPlugin(options = {}) {
|
|
|
19295
20547
|
const bundled = bundledAssetPath();
|
|
19296
20548
|
if (bundled) {
|
|
19297
20549
|
assertBundledPluginVersion(bundled);
|
|
19298
|
-
const dest2 =
|
|
20550
|
+
const dest2 = join10(pluginsFolder, ASSET_NAME);
|
|
19299
20551
|
if (filesMatch(bundled, dest2)) {
|
|
19300
20552
|
log(`${ASSET_NAME} already installed.`);
|
|
19301
20553
|
return;
|
|
19302
20554
|
}
|
|
19303
|
-
|
|
20555
|
+
copyFileSync3(bundled, dest2);
|
|
19304
20556
|
log(`Installed bundled ${ASSET_NAME} to ${dest2}`);
|
|
19305
20557
|
return;
|
|
19306
20558
|
}
|
|
19307
20559
|
log(dev ? "Fetching latest dev prerelease..." : "Fetching latest release...");
|
|
19308
|
-
const
|
|
19309
|
-
const asset =
|
|
20560
|
+
const release2 = dev ? await findDevRelease() : await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
20561
|
+
const asset = release2.assets?.find((a) => a.name === ASSET_NAME);
|
|
19310
20562
|
if (!asset) {
|
|
19311
|
-
throw new Error(`${ASSET_NAME} not found in release ${
|
|
20563
|
+
throw new Error(`${ASSET_NAME} not found in release ${release2.tag_name}`);
|
|
19312
20564
|
}
|
|
19313
|
-
const dest =
|
|
19314
|
-
log(`Downloading ${ASSET_NAME} from ${
|
|
20565
|
+
const dest = join10(pluginsFolder, ASSET_NAME);
|
|
20566
|
+
log(`Downloading ${ASSET_NAME} from ${release2.tag_name}...`);
|
|
19315
20567
|
await download(asset.browser_download_url, dest);
|
|
19316
20568
|
log(`Installed to ${dest}`);
|
|
19317
20569
|
}
|
|
@@ -19337,18 +20589,62 @@ var argFlagValue = (flag) => {
|
|
|
19337
20589
|
};
|
|
19338
20590
|
var portArg = argFlagValue("--port");
|
|
19339
20591
|
if (portArg) process.env.ROBLOX_STUDIO_PORT = portArg;
|
|
20592
|
+
var hostArg = argFlagValue("--host");
|
|
20593
|
+
if (hostArg) process.env.ROBLOX_STUDIO_HOST = hostArg;
|
|
20594
|
+
var sessionTokenArg = argFlagValue("--session-token");
|
|
20595
|
+
if (sessionTokenArg) process.env.BLOXFORGE_SESSION_TOKEN = sessionTokenArg;
|
|
19340
20596
|
if (process.argv.includes("--debug")) process.env.ROBLOX_STUDIO_DEBUG = "1";
|
|
19341
|
-
if (process.argv.includes("--
|
|
20597
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
20598
|
+
console.log(`BloxForge MCP Server
|
|
20599
|
+
|
|
20600
|
+
Usage:
|
|
20601
|
+
npx @princeofscale/bloxforge [options]
|
|
20602
|
+
npx @princeofscale/bloxforge <command>
|
|
20603
|
+
|
|
20604
|
+
Options:
|
|
20605
|
+
--port <port> Port to run the HTTP bridge on (default: 58741)
|
|
20606
|
+
--host <host> Bind host (default: 127.0.0.1; non-loopback is unsafe)
|
|
20607
|
+
--session-token <token> Require bearer auth for /proxy and /mcp HTTP clients
|
|
20608
|
+
--debug Enable debug logging (stack traces, verbose output)
|
|
20609
|
+
--open-cloud-key <key> Roblox Open Cloud API Key (for some advanced tools)
|
|
20610
|
+
--pollinations-key <key> Pollinations API Key (for image-generation tools)
|
|
20611
|
+
--creator-id <id> Roblox Creator User ID
|
|
20612
|
+
--creator-group-id <id> Roblox Creator Group ID
|
|
20613
|
+
--profile <profile> Load specific tool profile (e.g., core)
|
|
20614
|
+
--help, -h Show this help message
|
|
20615
|
+
|
|
20616
|
+
Commands:
|
|
20617
|
+
verify, --doctor Run diagnostics to verify installation and connection
|
|
20618
|
+
report Generate a detailed diagnostic report for bug reports
|
|
20619
|
+
--install-plugin Manually install the Studio plugin to your local Roblox directory
|
|
20620
|
+
--auto-install-plugin Silently install the plugin (used by AI clients automatically)
|
|
20621
|
+
`);
|
|
20622
|
+
process.exit(0);
|
|
20623
|
+
}
|
|
20624
|
+
if (process.argv.includes("--doctor") || process.argv.includes("verify")) {
|
|
19342
20625
|
const require2 = createRequire(import.meta.url);
|
|
19343
20626
|
const { version } = require2("../package.json");
|
|
19344
20627
|
process.exitCode = await runDoctor({
|
|
19345
20628
|
version,
|
|
19346
20629
|
port: portArg ? parseInt(portArg) : void 0
|
|
19347
20630
|
});
|
|
20631
|
+
} else if (process.argv.includes("--report") || process.argv.includes("report")) {
|
|
20632
|
+
const require2 = createRequire(import.meta.url);
|
|
20633
|
+
const { version } = require2("../package.json");
|
|
20634
|
+
const reportText = await generateDiagnosticReport({
|
|
20635
|
+
version,
|
|
20636
|
+
port: portArg ? parseInt(portArg) : void 0
|
|
20637
|
+
});
|
|
20638
|
+
console.log(reportText);
|
|
20639
|
+
process.exit(0);
|
|
19348
20640
|
} else if (process.argv.includes("--install-plugin")) {
|
|
19349
20641
|
const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
|
|
19350
20642
|
await installPlugin2().catch((err) => {
|
|
19351
|
-
console.error(
|
|
20643
|
+
console.error(`[install-plugin] What happened: Failed to install plugin.`);
|
|
20644
|
+
console.error(`[install-plugin] Why: ${err instanceof Error ? err.message : String(err)}`);
|
|
20645
|
+
console.error(`[install-plugin] Fix: Ensure your Roblox Studio plugins folder is accessible and you have write permissions.`);
|
|
20646
|
+
console.error(`[install-plugin] Verify: Run 'npx @princeofscale/bloxforge verify' to check if the plugin is installed.`);
|
|
20647
|
+
if (process.env.ROBLOX_STUDIO_DEBUG) console.error(err);
|
|
19352
20648
|
process.exitCode = 1;
|
|
19353
20649
|
});
|
|
19354
20650
|
} else {
|
|
@@ -19359,7 +20655,7 @@ if (process.argv.includes("--doctor")) {
|
|
|
19359
20655
|
warn: (message) => console.error(message)
|
|
19360
20656
|
}).catch((err) => {
|
|
19361
20657
|
console.error(
|
|
19362
|
-
`[install-plugin] Auto-install skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
20658
|
+
`[install-plugin] Auto-install skipped. Why: ${err instanceof Error ? err.message : String(err)}`
|
|
19363
20659
|
);
|
|
19364
20660
|
});
|
|
19365
20661
|
}
|
|
@@ -19385,7 +20681,12 @@ if (process.argv.includes("--doctor")) {
|
|
|
19385
20681
|
tools: getAllTools()
|
|
19386
20682
|
});
|
|
19387
20683
|
server.run().catch((error) => {
|
|
19388
|
-
console.error(
|
|
20684
|
+
console.error(`
|
|
20685
|
+
[Fatal Error] What happened: BloxForge Server failed to start.`);
|
|
20686
|
+
console.error(`[Fatal Error] Why: ${error instanceof Error ? error.message : String(error)}`);
|
|
20687
|
+
console.error(`[Fatal Error] Fix: Check if another instance is already running on port ${process.env.ROBLOX_STUDIO_PORT || 58741}.`);
|
|
20688
|
+
console.error(`[Fatal Error] Verify: Run 'npx @princeofscale/bloxforge verify' to diagnose the issue.`);
|
|
20689
|
+
if (process.env.ROBLOX_STUDIO_DEBUG) console.error(error);
|
|
19389
20690
|
process.exit(1);
|
|
19390
20691
|
});
|
|
19391
20692
|
}
|