@princeofscale/bloxforge-inspector 3.0.0-rc.1 → 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 +1455 -378
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +380 -84
- package/studio-plugin/MCPPlugin.rbxmx +380 -84
- package/studio-plugin/src/modules/ClientBroker.ts +67 -14
- package/studio-plugin/src/modules/Communication.ts +151 -22
- 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 path9 = defaultRequestJournalPath();
|
|
224
|
+
if (path9)
|
|
225
|
+
this.journal = new RequestJournal(path9);
|
|
226
|
+
}
|
|
227
|
+
if (this.journal) {
|
|
228
|
+
try {
|
|
229
|
+
const snapshot = this.journal.load();
|
|
230
|
+
if (snapshot) {
|
|
231
|
+
const { statuses, pending } = snapshot;
|
|
232
|
+
for (const s of statuses) {
|
|
233
|
+
if (s.state === "delivered" || s.state === "started") {
|
|
234
|
+
s.state = "outcome_unknown";
|
|
235
|
+
s.updatedAt = Date.now();
|
|
236
|
+
}
|
|
237
|
+
this.requestStatuses.set(s.requestId, s);
|
|
238
|
+
}
|
|
239
|
+
for (const p of pending) {
|
|
240
|
+
if (p.state !== "queued")
|
|
241
|
+
continue;
|
|
242
|
+
this.pendingRequests.set(p.id, {
|
|
243
|
+
id: p.id,
|
|
244
|
+
endpoint: p.endpoint,
|
|
245
|
+
data: p.data,
|
|
246
|
+
targetInstanceId: p.targetInstanceId,
|
|
247
|
+
targetRole: p.targetRole,
|
|
248
|
+
timestamp: p.timestamp,
|
|
249
|
+
inFlight: false,
|
|
250
|
+
deliveryAttempt: p.deliveryAttempt,
|
|
251
|
+
resolve: () => {
|
|
252
|
+
},
|
|
253
|
+
reject: () => {
|
|
254
|
+
},
|
|
255
|
+
timeoutId: setTimeout(() => {
|
|
256
|
+
}, 0)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch (e) {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
enableJournal() {
|
|
265
|
+
const path9 = defaultRequestJournalPath();
|
|
266
|
+
if (path9)
|
|
267
|
+
this.journal = new RequestJournal(path9);
|
|
268
|
+
}
|
|
269
|
+
persistJournal() {
|
|
270
|
+
if (!this.journal)
|
|
271
|
+
return;
|
|
272
|
+
this.journal.save(Array.from(this.requestStatuses.entries()).map(([id, s]) => ({ requestId: id, ...s })), Array.from(this.pendingRequests.values()).map((req) => ({
|
|
273
|
+
id: req.id,
|
|
274
|
+
endpoint: req.endpoint,
|
|
275
|
+
data: req.data,
|
|
276
|
+
targetInstanceId: req.targetInstanceId,
|
|
277
|
+
targetRole: req.targetRole,
|
|
278
|
+
timestamp: req.timestamp,
|
|
279
|
+
state: this.requestStatuses.get(req.id)?.state ?? "queued",
|
|
280
|
+
deliveryAttempt: req.deliveryAttempt ?? 0
|
|
281
|
+
})), []);
|
|
282
|
+
}
|
|
283
|
+
updateRequestStatus(requestId, status) {
|
|
284
|
+
const existing = this.requestStatuses.get(requestId) ?? { state: "queued", serverEpoch: this.serverEpoch, deliveryAttempt: 0, updatedAt: Date.now() };
|
|
285
|
+
this.requestStatuses.set(requestId, { ...existing, ...status, updatedAt: Date.now() });
|
|
286
|
+
}
|
|
89
287
|
canonicalInstanceId(instanceId, placeId) {
|
|
90
288
|
return publishedInstanceId(placeId) ?? instanceId;
|
|
91
289
|
}
|
|
@@ -212,30 +410,53 @@ var init_bridge_service = __esm({
|
|
|
212
410
|
lastActivity: Date.now(),
|
|
213
411
|
connectedAt: prior?.connectedAt ?? Date.now()
|
|
214
412
|
});
|
|
215
|
-
|
|
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" });
|
|
494
786
|
}
|
|
495
787
|
}
|
|
788
|
+
this.persistJournal();
|
|
789
|
+
}
|
|
790
|
+
reconcilePluginReceipts(pluginSessionId, serverEpoch, receipts) {
|
|
791
|
+
if (serverEpoch !== this.serverEpoch)
|
|
792
|
+
return 0;
|
|
793
|
+
const instance = this.instances.get(pluginSessionId);
|
|
794
|
+
if (!instance)
|
|
795
|
+
return 0;
|
|
796
|
+
let reconciled = 0;
|
|
797
|
+
for (const receipt of receipts) {
|
|
798
|
+
const status = this.requestStatuses.get(receipt.requestId);
|
|
799
|
+
if (status && status.state === "outcome_unknown") {
|
|
800
|
+
status.state = "completed";
|
|
801
|
+
status.updatedAt = receipt.completedAt || Date.now();
|
|
802
|
+
status.response = { reconciled: true, digest: receipt.responseDigest };
|
|
803
|
+
reconciled++;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (reconciled > 0) {
|
|
807
|
+
this.persistJournal();
|
|
808
|
+
}
|
|
809
|
+
return reconciled;
|
|
810
|
+
}
|
|
811
|
+
acknowledgeRequest(requestId) {
|
|
812
|
+
const status = this.requestStatuses.get(requestId);
|
|
813
|
+
if (!status)
|
|
814
|
+
return false;
|
|
815
|
+
this.updateRequestStatus(requestId, { state: "started" });
|
|
816
|
+
this.persistJournal();
|
|
817
|
+
return true;
|
|
496
818
|
}
|
|
497
819
|
resolveRequest(requestId, response) {
|
|
498
820
|
const request = this.pendingRequests.get(requestId);
|
|
821
|
+
this.updateRequestStatus(requestId, { state: "completed", response, error: void 0 });
|
|
822
|
+
this.transportCounters.completed++;
|
|
499
823
|
if (request) {
|
|
500
824
|
clearTimeout(request.timeoutId);
|
|
501
825
|
this.pendingRequests.delete(requestId);
|
|
502
826
|
request.resolve(response);
|
|
503
827
|
}
|
|
828
|
+
this.persistJournal();
|
|
504
829
|
}
|
|
505
830
|
rejectRequest(requestId, error) {
|
|
506
831
|
const request = this.pendingRequests.get(requestId);
|
|
832
|
+
this.updateRequestStatus(requestId, { state: "failed", error });
|
|
507
833
|
if (request) {
|
|
508
834
|
clearTimeout(request.timeoutId);
|
|
509
835
|
this.pendingRequests.delete(requestId);
|
|
510
836
|
request.reject(error);
|
|
511
837
|
}
|
|
838
|
+
this.persistJournal();
|
|
839
|
+
}
|
|
840
|
+
resolveFencedRequest(requestId, response, fence) {
|
|
841
|
+
const status = this.requestStatuses.get(requestId);
|
|
842
|
+
const pending = this.pendingRequests.get(requestId);
|
|
843
|
+
if (!status || status.serverEpoch !== fence.serverEpoch || status.deliveryAttempt !== fence.deliveryAttempt || status.leaseToken !== fence.leaseToken || status.state === "completed" || status.state === "failed" || pending?.assignedPluginSessionId !== fence.pluginSessionId)
|
|
844
|
+
return false;
|
|
845
|
+
this.resolveRequest(requestId, response);
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
rejectFencedRequest(requestId, error, fence) {
|
|
849
|
+
const status = this.requestStatuses.get(requestId);
|
|
850
|
+
const pending = this.pendingRequests.get(requestId);
|
|
851
|
+
if (!status || status.serverEpoch !== fence.serverEpoch || status.deliveryAttempt !== fence.deliveryAttempt || status.leaseToken !== fence.leaseToken || status.state === "completed" || status.state === "failed" || pending?.assignedPluginSessionId !== fence.pluginSessionId)
|
|
852
|
+
return false;
|
|
853
|
+
this.rejectRequest(requestId, error);
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
acknowledgeFencedRequest(requestId, fence) {
|
|
857
|
+
const status = this.requestStatuses.get(requestId);
|
|
858
|
+
const pending = this.pendingRequests.get(requestId);
|
|
859
|
+
if (!status || status.serverEpoch !== fence.serverEpoch || status.deliveryAttempt !== fence.deliveryAttempt || status.leaseToken !== fence.leaseToken || status.state === "completed" || status.state === "failed" || pending?.assignedPluginSessionId !== fence.pluginSessionId)
|
|
860
|
+
return false;
|
|
861
|
+
return this.acknowledgeRequest(requestId);
|
|
512
862
|
}
|
|
513
863
|
cleanupOldRequests() {
|
|
514
864
|
const now = Date.now();
|
|
@@ -527,6 +877,48 @@ var init_bridge_service = __esm({
|
|
|
527
877
|
}
|
|
528
878
|
this.pendingRequests.clear();
|
|
529
879
|
}
|
|
880
|
+
cancelRequest(requestId) {
|
|
881
|
+
const request = this.pendingRequests.get(requestId);
|
|
882
|
+
if (!request)
|
|
883
|
+
return false;
|
|
884
|
+
const status = this.requestStatuses.get(requestId);
|
|
885
|
+
if (status?.state === "started") {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
clearTimeout(request.timeoutId);
|
|
889
|
+
this.pendingRequests.delete(requestId);
|
|
890
|
+
this.updateRequestStatus(requestId, { state: "cancelled", error: "Request cancelled before execution" });
|
|
891
|
+
this.transportCounters.cancelled++;
|
|
892
|
+
request.reject(new Error(`Request ${requestId} cancelled`));
|
|
893
|
+
this.persistJournal();
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
getCancellationEvents(pluginSessionId) {
|
|
897
|
+
const instance = this.instances.get(pluginSessionId);
|
|
898
|
+
if (!instance)
|
|
899
|
+
return [];
|
|
900
|
+
const events = [];
|
|
901
|
+
for (const request of this.pendingRequests.values()) {
|
|
902
|
+
if (request.targetInstanceId === instance.instanceId && request.targetRole === instance.role && this.requestStatuses.get(request.id)?.state === "started" && request.cancellationRequested) {
|
|
903
|
+
events.push(request.id);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return events;
|
|
907
|
+
}
|
|
908
|
+
async requestCancellation(requestId) {
|
|
909
|
+
const request = this.pendingRequests.get(requestId);
|
|
910
|
+
if (!request)
|
|
911
|
+
return false;
|
|
912
|
+
const status = this.requestStatuses.get(requestId);
|
|
913
|
+
if (status?.state === "started") {
|
|
914
|
+
request.cancellationRequested = true;
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
return this.cancelRequest(requestId);
|
|
918
|
+
}
|
|
919
|
+
isMutation(endpoint) {
|
|
920
|
+
return !endpoint.startsWith("/api/read") && !endpoint.startsWith("/api/get") && !endpoint.startsWith("/api/search") && endpoint !== "/api/evaluate" && endpoint !== "/api/execute-script" && endpoint !== "/api/run-code";
|
|
921
|
+
}
|
|
530
922
|
};
|
|
531
923
|
}
|
|
532
924
|
});
|
|
@@ -593,7 +985,12 @@ function isRetryable(code) {
|
|
|
593
985
|
}
|
|
594
986
|
function toolErrorResult(error, stage) {
|
|
595
987
|
const message = error instanceof Error ? error.message : String(error);
|
|
596
|
-
const
|
|
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 path9 = decodeURIComponent(segments.slice(1).join("/"));
|
|
1326
|
+
return { kind: "node", path: path9 };
|
|
891
1327
|
}
|
|
892
1328
|
if (segments[0] === "playtest" && segments[1] === "episode" && segments.length >= 3) {
|
|
893
1329
|
return { kind: "episode", id: decodeURIComponent(segments.slice(2).join("/")) };
|
|
@@ -1104,6 +1540,20 @@ var init_tool_catalog = __esm({
|
|
|
1104
1540
|
"load_toolset",
|
|
1105
1541
|
"get_roblox_docs",
|
|
1106
1542
|
"get_session_summary",
|
|
1543
|
+
"get_request_status",
|
|
1544
|
+
"get_transport_diagnostics",
|
|
1545
|
+
"cancel_request",
|
|
1546
|
+
"detect_roblox_project",
|
|
1547
|
+
"validate_script_source",
|
|
1548
|
+
"format_script_preview",
|
|
1549
|
+
"resolve_instance_source_file",
|
|
1550
|
+
"run_project_tests",
|
|
1551
|
+
"get_dependency_graph",
|
|
1552
|
+
"install_wally_packages",
|
|
1553
|
+
"run_quality_gate",
|
|
1554
|
+
"validate_with_luau_lsp",
|
|
1555
|
+
"generate_rojo_sourcemap",
|
|
1556
|
+
"build_rojo_project",
|
|
1107
1557
|
"get_connected_instances",
|
|
1108
1558
|
"get_scene_summary",
|
|
1109
1559
|
"execute_luau",
|
|
@@ -1209,7 +1659,6 @@ var init_tool_catalog = __esm({
|
|
|
1209
1659
|
|
|
1210
1660
|
// ../core/dist/http-server.js
|
|
1211
1661
|
import express from "express";
|
|
1212
|
-
import cors from "cors";
|
|
1213
1662
|
import http from "http";
|
|
1214
1663
|
import { WebSocketServer, WebSocket } from "ws";
|
|
1215
1664
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -1217,6 +1666,23 @@ 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;
|
|
@@ -1250,9 +1716,9 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1250
1716
|
const isLazyTools = () => registry ? registry.lazyMode : !(envLazyFlag === "0" || envLazyFlag === "false" || envLazyFlag === "off");
|
|
1251
1717
|
const getActiveToolCount = () => isLazyTools() ? registry ? registry.activeNames.size : CORE_TOOLS.size : serverConfig?.tools.length ?? 0;
|
|
1252
1718
|
const getLoadedToolsets = () => isLazyTools() ? [process.env.BLOXFORGE_TOOL_PROFILE?.trim().toLowerCase() || "core"] : ["all"];
|
|
1253
|
-
|
|
1254
|
-
app.use(express.json({ limit:
|
|
1255
|
-
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 }));
|
|
1256
1722
|
app.get("/health", (req, res) => {
|
|
1257
1723
|
const instances = bridge.getInstances();
|
|
1258
1724
|
const publicInstances = instances.map(toPublic);
|
|
@@ -1355,6 +1821,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1355
1821
|
success: true,
|
|
1356
1822
|
assignedRole: result.assignedRole,
|
|
1357
1823
|
instanceId: result.instanceId,
|
|
1824
|
+
sessionToken: result.sessionToken,
|
|
1825
|
+
serverEpoch: bridge.serverEpoch,
|
|
1358
1826
|
serverVersion: serverConfig?.version,
|
|
1359
1827
|
serverProtocolVersion: MCP_PROTOCOL_VERSION,
|
|
1360
1828
|
versionMismatch: registered?.versionMismatch ?? false,
|
|
@@ -1364,6 +1832,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1364
1832
|
});
|
|
1365
1833
|
app.post("/disconnect", (req, res) => {
|
|
1366
1834
|
const { pluginSessionId } = req.body;
|
|
1835
|
+
if (!pluginAuthorized(req, pluginSessionId)) {
|
|
1836
|
+
res.status(401).json({ success: false, error: "invalid_session_token" });
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1367
1839
|
if (pluginSessionId) {
|
|
1368
1840
|
bridge.unregisterInstance(pluginSessionId, "plugin_request");
|
|
1369
1841
|
warnedVersionMismatches.delete(pluginSessionId);
|
|
@@ -1380,6 +1852,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1380
1852
|
instances: publicInstances,
|
|
1381
1853
|
serverVersion: serverConfig?.version,
|
|
1382
1854
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
1855
|
+
serverEpoch: bridge.serverEpoch,
|
|
1383
1856
|
versionMismatch: publicInstances.some((inst) => inst.versionMismatch),
|
|
1384
1857
|
protocolMismatch: publicInstances.some((inst) => inst.protocolMismatch),
|
|
1385
1858
|
lazyTools: isLazyTools(),
|
|
@@ -1435,6 +1908,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1435
1908
|
});
|
|
1436
1909
|
app.get("/poll", (req, res) => {
|
|
1437
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
|
+
}
|
|
1438
1915
|
if (pluginSessionId) {
|
|
1439
1916
|
bridge.updateInstanceActivity(pluginSessionId);
|
|
1440
1917
|
}
|
|
@@ -1476,11 +1953,14 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1476
1953
|
});
|
|
1477
1954
|
return;
|
|
1478
1955
|
}
|
|
1479
|
-
const pendingRequest = knownInstance &&
|
|
1956
|
+
const pendingRequest = knownInstance && pluginSessionId ? bridge.getPendingRequestForSession(pluginSessionId) : null;
|
|
1480
1957
|
if (pendingRequest) {
|
|
1481
1958
|
res.json({
|
|
1482
1959
|
request: pendingRequest.request,
|
|
1483
1960
|
requestId: pendingRequest.requestId,
|
|
1961
|
+
serverEpoch: pendingRequest.serverEpoch,
|
|
1962
|
+
deliveryAttempt: pendingRequest.deliveryAttempt,
|
|
1963
|
+
leaseToken: pendingRequest.leaseToken,
|
|
1484
1964
|
mcpConnected: true,
|
|
1485
1965
|
pluginConnected: true,
|
|
1486
1966
|
knownInstance,
|
|
@@ -1491,7 +1971,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1491
1971
|
protocolVersion: callerPluginProtocolVersion,
|
|
1492
1972
|
versionMismatch,
|
|
1493
1973
|
protocolMismatch,
|
|
1494
|
-
proxyInstanceCount: proxyInstances.size
|
|
1974
|
+
proxyInstanceCount: proxyInstances.size,
|
|
1975
|
+
cancellations: pluginSessionId ? bridge.getCancellationEvents(pluginSessionId) : []
|
|
1495
1976
|
});
|
|
1496
1977
|
} else {
|
|
1497
1978
|
res.json({
|
|
@@ -1506,16 +1987,79 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1506
1987
|
protocolVersion: callerPluginProtocolVersion,
|
|
1507
1988
|
versionMismatch,
|
|
1508
1989
|
protocolMismatch,
|
|
1509
|
-
proxyInstanceCount: proxyInstances.size
|
|
1990
|
+
proxyInstanceCount: proxyInstances.size,
|
|
1991
|
+
cancellations: pluginSessionId ? bridge.getCancellationEvents(pluginSessionId) : []
|
|
1510
1992
|
});
|
|
1511
1993
|
}
|
|
1512
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
|
+
});
|
|
1513
2051
|
app.post("/response", (req, res) => {
|
|
1514
|
-
const { requestId, response, error } = req.body;
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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;
|
|
1519
2063
|
}
|
|
1520
2064
|
res.json({ success: true });
|
|
1521
2065
|
});
|
|
@@ -1532,7 +2076,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1532
2076
|
const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole);
|
|
1533
2077
|
res.json({ response });
|
|
1534
2078
|
} catch (err) {
|
|
1535
|
-
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
|
+
});
|
|
1536
2083
|
}
|
|
1537
2084
|
});
|
|
1538
2085
|
if (serverConfig) {
|
|
@@ -1563,6 +2110,12 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1563
2110
|
if (allowedTools && !allowedTools.has(name)) {
|
|
1564
2111
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
1565
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
|
+
}
|
|
1566
2119
|
try {
|
|
1567
2120
|
if (registry) {
|
|
1568
2121
|
const registryResult = await registry.callTool(name, tools, args || {});
|
|
@@ -1694,12 +2247,12 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
|
|
|
1694
2247
|
return app;
|
|
1695
2248
|
}
|
|
1696
2249
|
function listenWithRetry(app, host, startPort, maxAttempts = 5) {
|
|
1697
|
-
return new Promise(async (
|
|
2250
|
+
return new Promise(async (resolve7, reject) => {
|
|
1698
2251
|
for (let i = 0; i < maxAttempts; i++) {
|
|
1699
2252
|
const port = startPort + i;
|
|
1700
2253
|
try {
|
|
1701
2254
|
const server = await bindPort(app, host, port);
|
|
1702
|
-
|
|
2255
|
+
resolve7({ server, port });
|
|
1703
2256
|
return;
|
|
1704
2257
|
} catch (err) {
|
|
1705
2258
|
if (err.code === "EADDRINUSE") {
|
|
@@ -1714,7 +2267,7 @@ function listenWithRetry(app, host, startPort, maxAttempts = 5) {
|
|
|
1714
2267
|
});
|
|
1715
2268
|
}
|
|
1716
2269
|
function bindPort(app, host, port) {
|
|
1717
|
-
return new Promise((
|
|
2270
|
+
return new Promise((resolve7, reject) => {
|
|
1718
2271
|
const server = http.createServer(app);
|
|
1719
2272
|
attachBridgeWebSocket(server, app.bridge);
|
|
1720
2273
|
const onError = (err) => {
|
|
@@ -1724,11 +2277,12 @@ function bindPort(app, host, port) {
|
|
|
1724
2277
|
server.once("error", onError);
|
|
1725
2278
|
server.listen(port, host, () => {
|
|
1726
2279
|
server.removeListener("error", onError);
|
|
1727
|
-
|
|
2280
|
+
resolve7(server);
|
|
1728
2281
|
});
|
|
1729
2282
|
});
|
|
1730
2283
|
}
|
|
1731
2284
|
function attachBridgeWebSocket(server, bridge) {
|
|
2285
|
+
const requirePluginAuth = process.env.NODE_ENV !== "test";
|
|
1732
2286
|
const streams = /* @__PURE__ */ new Map();
|
|
1733
2287
|
const wss = new WebSocketServer({ noServer: true });
|
|
1734
2288
|
const deliver = (pluginSessionId) => {
|
|
@@ -1749,7 +2303,8 @@ function attachBridgeWebSocket(server, bridge) {
|
|
|
1749
2303
|
if (url.pathname !== "/stream")
|
|
1750
2304
|
return;
|
|
1751
2305
|
const pluginSessionId = url.searchParams.get("pluginSessionId");
|
|
1752
|
-
|
|
2306
|
+
const sessionToken = url.searchParams.get("sessionToken") ?? void 0;
|
|
2307
|
+
if (!pluginSessionId || !bridge.getInstanceBySessionId(pluginSessionId) || requirePluginAuth && !bridge.authenticatePlugin(pluginSessionId, sessionToken ?? "")) {
|
|
1753
2308
|
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
1754
2309
|
socket.destroy();
|
|
1755
2310
|
return;
|
|
@@ -1764,9 +2319,30 @@ function attachBridgeWebSocket(server, bridge) {
|
|
|
1764
2319
|
bridge.updateInstanceActivity(pluginSessionId);
|
|
1765
2320
|
try {
|
|
1766
2321
|
const message = JSON.parse(raw.toString());
|
|
1767
|
-
if (
|
|
2322
|
+
if (typeof message.requestId !== "string")
|
|
2323
|
+
return;
|
|
2324
|
+
const fence = {
|
|
2325
|
+
serverEpoch: message.serverEpoch,
|
|
2326
|
+
pluginSessionId,
|
|
2327
|
+
deliveryAttempt: message.deliveryAttempt,
|
|
2328
|
+
leaseToken: message.leaseToken
|
|
2329
|
+
};
|
|
2330
|
+
const requiresFence = (bridge.getInstanceBySessionId(pluginSessionId)?.pluginProtocolVersion ?? 0) >= 3;
|
|
2331
|
+
if (message.type === "ack") {
|
|
2332
|
+
if (requiresFence)
|
|
2333
|
+
bridge.acknowledgeFencedRequest(message.requestId, fence);
|
|
2334
|
+
else
|
|
2335
|
+
bridge.acknowledgeRequest(message.requestId);
|
|
1768
2336
|
return;
|
|
1769
|
-
|
|
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)
|
|
1770
2346
|
bridge.rejectRequest(message.requestId, message.error);
|
|
1771
2347
|
else
|
|
1772
2348
|
bridge.resolveRequest(message.requestId, message.response);
|
|
@@ -1788,12 +2364,27 @@ var init_http_server = __esm({
|
|
|
1788
2364
|
init_tool_shape();
|
|
1789
2365
|
init_errors();
|
|
1790
2366
|
init_structured_output();
|
|
2367
|
+
init_capability_policy();
|
|
1791
2368
|
init_server_instructions();
|
|
1792
2369
|
init_resources();
|
|
1793
2370
|
init_tool_catalog();
|
|
1794
2371
|
TOOL_HANDLERS = {
|
|
1795
2372
|
get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
|
|
1796
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),
|
|
1797
2388
|
tool_catalog_search: (tools, body) => tools.toolCatalogSearch(body),
|
|
1798
2389
|
load_toolset: (tools, body) => tools.loadToolset(body),
|
|
1799
2390
|
get_world_snapshot: (tools, body) => tools.getWorldSnapshot(body.path, body.level, body.topNPerClass, body.instance_id),
|
|
@@ -1801,7 +2392,7 @@ var init_http_server = __esm({
|
|
|
1801
2392
|
get_changes_since: (tools, body) => tools.getChangesSince(body.snapshotId, body.path, body.instance_id),
|
|
1802
2393
|
scene_search: (tools, body) => tools.sceneSearch(body.query, body.path, body.limit, body.instance_id),
|
|
1803
2394
|
playtest_sample_state: (tools, body) => tools.playtestSampleState(body.domains, body.target, body.instance_id),
|
|
1804
|
-
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),
|
|
1805
2396
|
list_recipes: (tools) => tools.listRecipes(),
|
|
1806
2397
|
apply_recipe: (tools, body) => tools.applyRecipe(body.recipe, body.params, body.instance_id),
|
|
1807
2398
|
run_gameplay_assertions: (tools, body) => tools.runGameplayAssertions(body.assertions, body.target, body.instance_id),
|
|
@@ -2187,8 +2778,8 @@ var init_safety_manager = __esm({
|
|
|
2187
2778
|
}
|
|
2188
2779
|
return { allowed, requiresConfirmation, blocked, dryRun: false, reasons, warnings, matchedPatterns };
|
|
2189
2780
|
}
|
|
2190
|
-
isProtectedPath(
|
|
2191
|
-
const trimmed =
|
|
2781
|
+
isProtectedPath(path9) {
|
|
2782
|
+
const trimmed = path9.trim();
|
|
2192
2783
|
return this.config.protectedPaths.some((p) => p === trimmed || trimmed === `game.${p}`);
|
|
2193
2784
|
}
|
|
2194
2785
|
recordOperation(record) {
|
|
@@ -2201,10 +2792,10 @@ var init_safety_manager = __esm({
|
|
|
2201
2792
|
getHistory() {
|
|
2202
2793
|
return [...this.history].reverse();
|
|
2203
2794
|
}
|
|
2204
|
-
backupScript(
|
|
2205
|
-
const existing = this.backups.get(
|
|
2206
|
-
this.backups.set(
|
|
2207
|
-
path:
|
|
2795
|
+
backupScript(path9, source) {
|
|
2796
|
+
const existing = this.backups.get(path9);
|
|
2797
|
+
this.backups.set(path9, {
|
|
2798
|
+
path: path9,
|
|
2208
2799
|
source,
|
|
2209
2800
|
previous: existing?.source,
|
|
2210
2801
|
timestamp: Date.now()
|
|
@@ -2215,8 +2806,8 @@ var init_safety_manager = __esm({
|
|
|
2215
2806
|
this.backups.delete(oldestKey);
|
|
2216
2807
|
}
|
|
2217
2808
|
}
|
|
2218
|
-
getBackup(
|
|
2219
|
-
return this.backups.get(
|
|
2809
|
+
getBackup(path9) {
|
|
2810
|
+
return this.backups.get(path9);
|
|
2220
2811
|
}
|
|
2221
2812
|
listBackups() {
|
|
2222
2813
|
return [...this.backups.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
@@ -2697,6 +3288,7 @@ local parent = resolvePath(${luaString(parent)})
|
|
|
2697
3288
|
if not parent then return { error = "Parent not found: " .. ${luaString(parent)} } end
|
|
2698
3289
|
local existing = parent:FindFirstChild(${luaString(name)})
|
|
2699
3290
|
if existing then existing:Destroy() end
|
|
3291
|
+
if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
|
|
2700
3292
|
local door = Instance.new("Part")
|
|
2701
3293
|
door.Name = ${luaString(name)}
|
|
2702
3294
|
door.Size = Vector3.new(8, 10, 1)
|
|
@@ -2733,6 +3325,7 @@ local group = SoundService:FindFirstChild(groupName)
|
|
|
2733
3325
|
if not group then group = Instance.new("SoundGroup") group.Name = groupName group.Parent = SoundService end
|
|
2734
3326
|
local existing = SoundService:FindFirstChild(${luaString(name)})
|
|
2735
3327
|
if existing then existing:Destroy() end
|
|
3328
|
+
if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
|
|
2736
3329
|
local sound = Instance.new("Sound")
|
|
2737
3330
|
sound.Name = ${luaString(name)}
|
|
2738
3331
|
sound.SoundId = ${luaString(soundId)}
|
|
@@ -2753,6 +3346,7 @@ local parent = resolvePath(${luaString(parent)})
|
|
|
2753
3346
|
if not parent then return { error = "Parent not found: " .. ${luaString(parent)} } end
|
|
2754
3347
|
local existing = parent:FindFirstChild(${luaString(name)})
|
|
2755
3348
|
if existing then existing:Destroy() end
|
|
3349
|
+
if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
|
|
2756
3350
|
local brick = Instance.new("Part")
|
|
2757
3351
|
brick.Name = ${luaString(name)}
|
|
2758
3352
|
brick.Size = Vector3.new(8, 1, 8)
|
|
@@ -3404,12 +3998,14 @@ var init_mutation = __esm({
|
|
|
3404
3998
|
property: { type: "string", description: "For set_property." },
|
|
3405
3999
|
name: { type: "string", description: "For set_attribute." },
|
|
3406
4000
|
tag: { type: "string", description: "For add_tag / remove_tag." },
|
|
3407
|
-
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." }
|
|
3408
4003
|
},
|
|
3409
4004
|
required: ["op", "target"]
|
|
3410
4005
|
}
|
|
3411
4006
|
},
|
|
3412
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)." },
|
|
3413
4009
|
confirm: { type: "boolean", description: "Approve a large plan the safety layer gates." },
|
|
3414
4010
|
instance_id: { type: "string", description: "Connected Studio place id. Required only when multiple places are open." }
|
|
3415
4011
|
},
|
|
@@ -6828,6 +7424,102 @@ var init_meta = __esm({
|
|
|
6828
7424
|
properties: {}
|
|
6829
7425
|
}
|
|
6830
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
|
+
},
|
|
6831
7523
|
{
|
|
6832
7524
|
name: "load_toolset",
|
|
6833
7525
|
category: "read",
|
|
@@ -7357,8 +8049,8 @@ for _, p in ipairs(paths) do
|
|
|
7357
8049
|
end
|
|
7358
8050
|
return { nodes = out, count = #out }`;
|
|
7359
8051
|
}
|
|
7360
|
-
function buildWorldSnapshotLuau(
|
|
7361
|
-
const safePath = luaString(
|
|
8052
|
+
function buildWorldSnapshotLuau(path9 = "game", level = "overview", topNPerClass = 12) {
|
|
8053
|
+
const safePath = luaString(path9);
|
|
7362
8054
|
const safeTopN = luaNumber(Math.max(1, Math.floor(topNPerClass)));
|
|
7363
8055
|
return `${PATH_RESOLVER_LUA}
|
|
7364
8056
|
local root = resolvePath(${safePath})
|
|
@@ -7370,6 +8062,8 @@ local soundCount, soundPlaying, soundLooped = 0, 0, 0
|
|
|
7370
8062
|
local scriptCount, localScriptCount, moduleCount = 0, 0, 0
|
|
7371
8063
|
local taggedCount = 0
|
|
7372
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
|
|
7373
8067
|
total = total + 1
|
|
7374
8068
|
byClass[d.ClassName] = (byClass[d.ClassName] or 0) + 1
|
|
7375
8069
|
if d:IsA("Sound") then
|
|
@@ -7466,8 +8160,8 @@ end`;
|
|
|
7466
8160
|
});
|
|
7467
8161
|
|
|
7468
8162
|
// ../core/dist/builders/scene-search.js
|
|
7469
|
-
function buildSceneSearchLuau(query,
|
|
7470
|
-
const safePath = luaString(
|
|
8163
|
+
function buildSceneSearchLuau(query, path9 = "game", limit = 10) {
|
|
8164
|
+
const safePath = luaString(path9);
|
|
7471
8165
|
const safeQuery = luaString(query.toLowerCase());
|
|
7472
8166
|
const safeLimit = luaNumber(Math.max(1, Math.min(50, Math.floor(limit))));
|
|
7473
8167
|
return `${PATH_RESOLVER_LUA}
|
|
@@ -7487,6 +8181,8 @@ end
|
|
|
7487
8181
|
|
|
7488
8182
|
local scored = {}
|
|
7489
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
|
|
7490
8186
|
local name = lc(d.Name)
|
|
7491
8187
|
local class = lc(d.ClassName)
|
|
7492
8188
|
local parentName = d.Parent and lc(d.Parent.Name) or ""
|
|
@@ -7533,8 +8229,8 @@ var init_scene_search = __esm({
|
|
|
7533
8229
|
});
|
|
7534
8230
|
|
|
7535
8231
|
// ../core/dist/builders/world-fingerprint.js
|
|
7536
|
-
function buildWorldFingerprintLuau(
|
|
7537
|
-
const safePath = luaString(
|
|
8232
|
+
function buildWorldFingerprintLuau(path9 = "game", maxNodes = 8e3) {
|
|
8233
|
+
const safePath = luaString(path9);
|
|
7538
8234
|
const safeMax = luaNumber(Math.max(1, Math.floor(maxNodes)));
|
|
7539
8235
|
return `${PATH_RESOLVER_LUA}
|
|
7540
8236
|
${FINGERPRINT_HELPERS_LUA}
|
|
@@ -7544,6 +8240,8 @@ local fp = {}
|
|
|
7544
8240
|
local count = 0
|
|
7545
8241
|
local truncated = false
|
|
7546
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
|
|
7547
8245
|
if count >= ${safeMax} then truncated = true break end
|
|
7548
8246
|
local id = nid(d)
|
|
7549
8247
|
fp[id] = {
|
|
@@ -7709,9 +8407,9 @@ var init_world_changes = __esm({
|
|
|
7709
8407
|
this.seq += 1;
|
|
7710
8408
|
return `snap_${Date.now().toString(36)}_${this.seq}`;
|
|
7711
8409
|
}
|
|
7712
|
-
put(
|
|
8410
|
+
put(path9, fingerprint) {
|
|
7713
8411
|
const id = this.newId();
|
|
7714
|
-
this.snapshots.set(id, { id, path:
|
|
8412
|
+
this.snapshots.set(id, { id, path: path9, fingerprint, createdAt: Date.now() });
|
|
7715
8413
|
this.prune();
|
|
7716
8414
|
return id;
|
|
7717
8415
|
}
|
|
@@ -8771,7 +9469,7 @@ function encodeImageFromRgbaResponse(response, format, quality) {
|
|
|
8771
9469
|
};
|
|
8772
9470
|
}
|
|
8773
9471
|
function sleep(ms) {
|
|
8774
|
-
return new Promise((
|
|
9472
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
8775
9473
|
}
|
|
8776
9474
|
function errorMessage(error) {
|
|
8777
9475
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -9207,18 +9905,18 @@ var init_world_model_tools = __esm({
|
|
|
9207
9905
|
constructor(runtime) {
|
|
9208
9906
|
this.runtime = runtime;
|
|
9209
9907
|
}
|
|
9210
|
-
async getWorldSnapshot(
|
|
9211
|
-
const code = buildWorldSnapshotLuau(
|
|
9908
|
+
async getWorldSnapshot(path9, level, topNPerClass, instance_id) {
|
|
9909
|
+
const code = buildWorldSnapshotLuau(path9 ?? "game", level ?? "overview", topNPerClass ?? 12);
|
|
9212
9910
|
const response = await this.runtime.callSingle("/api/execute-luau", { code }, "edit", instance_id);
|
|
9213
9911
|
return wrapToolJsonText(normalizeExecuteLuauToolResult(response, {
|
|
9214
9912
|
error: "get_world_snapshot returned non-object execute-luau output"
|
|
9215
9913
|
}));
|
|
9216
9914
|
}
|
|
9217
|
-
async sceneSearch(query,
|
|
9915
|
+
async sceneSearch(query, path9, limit, instance_id) {
|
|
9218
9916
|
if (!query || !query.trim()) {
|
|
9219
9917
|
throw new Error("query is required for scene_search");
|
|
9220
9918
|
}
|
|
9221
|
-
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, path9 ?? "game", limit ?? 10) }, "edit", instance_id);
|
|
9222
9920
|
return wrapToolJsonText(normalizeExecuteLuauToolResult(response, {
|
|
9223
9921
|
query,
|
|
9224
9922
|
total: 0,
|
|
@@ -9238,8 +9936,8 @@ var init_world_model_tools = __esm({
|
|
|
9238
9936
|
error: "get_node_batch returned non-object execute-luau output"
|
|
9239
9937
|
}));
|
|
9240
9938
|
}
|
|
9241
|
-
async _captureFingerprint(
|
|
9242
|
-
const response = await this.runtime.callSingle("/api/execute-luau", { code: buildWorldFingerprintLuau(
|
|
9939
|
+
async _captureFingerprint(path9, instance_id) {
|
|
9940
|
+
const response = await this.runtime.callSingle("/api/execute-luau", { code: buildWorldFingerprintLuau(path9) }, "edit", instance_id);
|
|
9243
9941
|
try {
|
|
9244
9942
|
const rv = response?.returnValue;
|
|
9245
9943
|
if (typeof rv === "string") {
|
|
@@ -9252,8 +9950,8 @@ var init_world_model_tools = __esm({
|
|
|
9252
9950
|
}
|
|
9253
9951
|
return { fp: {}, count: 0, truncated: false, error: "Could not parse world fingerprint" };
|
|
9254
9952
|
}
|
|
9255
|
-
async getChangesSince(snapshotId,
|
|
9256
|
-
const p =
|
|
9953
|
+
async getChangesSince(snapshotId, path9, instance_id) {
|
|
9954
|
+
const p = path9 ?? "game";
|
|
9257
9955
|
const cur = await this._captureFingerprint(p, instance_id);
|
|
9258
9956
|
const wrap4 = (obj) => ({ content: [{ type: "text", text: JSON.stringify(obj) }] });
|
|
9259
9957
|
if (cur.error)
|
|
@@ -9417,8 +10115,8 @@ var init_response_shape = __esm({
|
|
|
9417
10115
|
});
|
|
9418
10116
|
|
|
9419
10117
|
// ../core/dist/builders/scene-summary.js
|
|
9420
|
-
function buildSceneSummaryLuau(
|
|
9421
|
-
const safePath = luaString(
|
|
10118
|
+
function buildSceneSummaryLuau(path9 = "game.Workspace", topN = 20) {
|
|
10119
|
+
const safePath = luaString(path9);
|
|
9422
10120
|
const safeTopN = luaNumber(Math.max(1, Math.floor(topN)));
|
|
9423
10121
|
return `${PATH_RESOLVER_LUA}
|
|
9424
10122
|
local root = resolvePath(${safePath})
|
|
@@ -9467,8 +10165,8 @@ var init_scene_read_tools = __esm({
|
|
|
9467
10165
|
constructor(runtime) {
|
|
9468
10166
|
this.runtime = runtime;
|
|
9469
10167
|
}
|
|
9470
|
-
async getFileTree(
|
|
9471
|
-
const response = await this.runtime.callSingle("/api/file-tree", { path:
|
|
10168
|
+
async getFileTree(path9 = "", instance_id) {
|
|
10169
|
+
const response = await this.runtime.callSingle("/api/file-tree", { path: path9 }, void 0, instance_id);
|
|
9472
10170
|
return compactText(response);
|
|
9473
10171
|
}
|
|
9474
10172
|
async getPlaceInfo(instance_id) {
|
|
@@ -9522,9 +10220,9 @@ var init_scene_read_tools = __esm({
|
|
|
9522
10220
|
const response = await this.runtime.callSingle("/api/class-info", { className }, void 0, instance_id);
|
|
9523
10221
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
9524
10222
|
}
|
|
9525
|
-
async getProjectStructure(
|
|
10223
|
+
async getProjectStructure(path9, maxDepth, scriptsOnly, instance_id) {
|
|
9526
10224
|
const response = await this.runtime.callSingle("/api/project-structure", {
|
|
9527
|
-
path:
|
|
10225
|
+
path: path9,
|
|
9528
10226
|
maxDepth,
|
|
9529
10227
|
scriptsOnly
|
|
9530
10228
|
}, void 0, instance_id);
|
|
@@ -9820,13 +10518,14 @@ ${JSON.stringify({ errors: result.errors, warnings: result.warnings })}`
|
|
|
9820
10518
|
});
|
|
9821
10519
|
|
|
9822
10520
|
// ../core/dist/builders/mutation-plan.js
|
|
9823
|
-
function buildMutationPlanLuau(operations, dryRun) {
|
|
10521
|
+
function buildMutationPlanLuau(operations, dryRun, atomic = true) {
|
|
9824
10522
|
const opsJson = JSON.stringify(JSON.stringify(operations));
|
|
9825
10523
|
return `${PATH_RESOLVER_LUA}
|
|
9826
10524
|
local HttpService = game:GetService("HttpService")
|
|
9827
10525
|
local CollectionService = game:GetService("CollectionService")
|
|
9828
10526
|
local ops = HttpService:JSONDecode(${opsJson})
|
|
9829
10527
|
local dryRun = ${luaBool(dryRun)}
|
|
10528
|
+
local atomic = ${luaBool(atomic)}
|
|
9830
10529
|
|
|
9831
10530
|
local function ser(v)
|
|
9832
10531
|
local t = typeof(v)
|
|
@@ -9837,8 +10536,26 @@ end
|
|
|
9837
10536
|
local results = {}
|
|
9838
10537
|
local rollback = {}
|
|
9839
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
|
|
9840
10556
|
|
|
9841
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
|
|
9842
10559
|
local r = { op = op.op, target = op.target }
|
|
9843
10560
|
local inst = resolvePath(op.target)
|
|
9844
10561
|
if not inst then
|
|
@@ -9895,9 +10612,26 @@ for _, op in ipairs(ops) do
|
|
|
9895
10612
|
table.insert(results, r)
|
|
9896
10613
|
end
|
|
9897
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
|
+
|
|
9898
10630
|
return {
|
|
9899
|
-
applied = not dryRun,
|
|
10631
|
+
applied = not dryRun and not rolledBack,
|
|
9900
10632
|
dryRun = dryRun,
|
|
10633
|
+
atomic = atomic,
|
|
10634
|
+
rolledBack = rolledBack,
|
|
9901
10635
|
results = results,
|
|
9902
10636
|
rollback = rollback,
|
|
9903
10637
|
summary = { total = #ops, succeeded = succeeded, failed = failed },
|
|
@@ -10059,7 +10793,7 @@ var init_mutation_tools = __esm({
|
|
|
10059
10793
|
}
|
|
10060
10794
|
// Transactional batch mutations: apply many small edits in one round-trip with a
|
|
10061
10795
|
// dry-run diff and a ready-to-run reverse plan in the receipt (stateless rollback).
|
|
10062
|
-
async applyMutationPlan(operations, dryRun, confirm, instance_id) {
|
|
10796
|
+
async applyMutationPlan(operations, dryRun, confirm, instance_id, atomic = true) {
|
|
10063
10797
|
if (!Array.isArray(operations) || operations.length === 0) {
|
|
10064
10798
|
throw new Error("operations (a non-empty array) is required for apply_mutation_plan");
|
|
10065
10799
|
}
|
|
@@ -10068,7 +10802,7 @@ var init_mutation_tools = __esm({
|
|
|
10068
10802
|
if (gated)
|
|
10069
10803
|
return gated;
|
|
10070
10804
|
}
|
|
10071
|
-
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);
|
|
10072
10806
|
if (!dryRun)
|
|
10073
10807
|
this.runtime.recordOperation("bulk_mutate", `mutation plan: ${operations.length} ops`);
|
|
10074
10808
|
return wrapToolJsonText(normalizeExecuteLuauToolResult(response, {
|
|
@@ -10578,9 +11312,9 @@ var init_image_client = __esm({
|
|
|
10578
11312
|
});
|
|
10579
11313
|
|
|
10580
11314
|
// ../core/dist/tools/asset-tools.js
|
|
10581
|
-
import * as
|
|
10582
|
-
import * as
|
|
10583
|
-
import * as
|
|
11315
|
+
import * as fs2 from "fs";
|
|
11316
|
+
import * as os2 from "os";
|
|
11317
|
+
import * as path2 from "path";
|
|
10584
11318
|
var AssetTools;
|
|
10585
11319
|
var init_asset_tools = __esm({
|
|
10586
11320
|
"../core/dist/tools/asset-tools.js"() {
|
|
@@ -10596,13 +11330,13 @@ var init_asset_tools = __esm({
|
|
|
10596
11330
|
}
|
|
10597
11331
|
// ─── Static helpers (library path) ────────────────────────────────
|
|
10598
11332
|
static findProjectRoot(startDir) {
|
|
10599
|
-
let dir =
|
|
11333
|
+
let dir = path2.resolve(startDir);
|
|
10600
11334
|
let previous = "";
|
|
10601
11335
|
while (dir !== previous) {
|
|
10602
|
-
if (
|
|
11336
|
+
if (fs2.existsSync(path2.join(dir, ".git")) || fs2.existsSync(path2.join(dir, "package.json")))
|
|
10603
11337
|
return dir;
|
|
10604
11338
|
previous = dir;
|
|
10605
|
-
dir =
|
|
11339
|
+
dir = path2.dirname(dir);
|
|
10606
11340
|
}
|
|
10607
11341
|
return null;
|
|
10608
11342
|
}
|
|
@@ -10610,22 +11344,22 @@ var init_asset_tools = __esm({
|
|
|
10610
11344
|
if (!candidate)
|
|
10611
11345
|
return false;
|
|
10612
11346
|
try {
|
|
10613
|
-
return
|
|
11347
|
+
return fs2.statSync(candidate).isDirectory();
|
|
10614
11348
|
} catch {
|
|
10615
11349
|
return false;
|
|
10616
11350
|
}
|
|
10617
11351
|
}
|
|
10618
11352
|
static ensureWritableDirectory(candidate, label) {
|
|
10619
|
-
const resolved =
|
|
11353
|
+
const resolved = path2.resolve(candidate);
|
|
10620
11354
|
try {
|
|
10621
|
-
|
|
11355
|
+
fs2.mkdirSync(resolved, { recursive: true });
|
|
10622
11356
|
} catch (error) {
|
|
10623
11357
|
throw new Error(`Unable to create ${label} build-library directory at ${resolved}: ${error.message}`);
|
|
10624
11358
|
}
|
|
10625
11359
|
if (!_AssetTools.isDirectory(resolved))
|
|
10626
11360
|
throw new Error(`${label} build-library path is not a directory: ${resolved}`);
|
|
10627
11361
|
try {
|
|
10628
|
-
|
|
11362
|
+
fs2.accessSync(resolved, fs2.constants.W_OK);
|
|
10629
11363
|
} catch (error) {
|
|
10630
11364
|
throw new Error(`${label} build-library directory is not writable: ${resolved}. ${error.message}`);
|
|
10631
11365
|
}
|
|
@@ -10636,27 +11370,27 @@ var init_asset_tools = __esm({
|
|
|
10636
11370
|
if (_AssetTools._cachedLibraryPath)
|
|
10637
11371
|
return _AssetTools._cachedLibraryPath;
|
|
10638
11372
|
const overridePath = process.env.BLOXFORGE_BUILD_LIBRARY || process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
|
|
10639
|
-
const cwd =
|
|
11373
|
+
const cwd = path2.resolve(process.cwd());
|
|
10640
11374
|
const projectRoot = _AssetTools.findProjectRoot(cwd);
|
|
10641
|
-
const newHomeLibraryPath =
|
|
10642
|
-
const legacyHomeLibraryPath =
|
|
10643
|
-
const homeLibraryPath =
|
|
10644
|
-
const projectLibraryPath = projectRoot ?
|
|
10645
|
-
const cwdLibraryPath =
|
|
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");
|
|
10646
11380
|
let result;
|
|
10647
11381
|
if (overridePath) {
|
|
10648
11382
|
result = _AssetTools.ensureWritableDirectory(overridePath, "override");
|
|
10649
11383
|
} else {
|
|
10650
11384
|
const existing = [projectLibraryPath, cwdLibraryPath].find((c) => c && _AssetTools.isDirectory(c) && (() => {
|
|
10651
11385
|
try {
|
|
10652
|
-
|
|
11386
|
+
fs2.accessSync(c, fs2.constants.W_OK);
|
|
10653
11387
|
return true;
|
|
10654
11388
|
} catch {
|
|
10655
11389
|
return false;
|
|
10656
11390
|
}
|
|
10657
11391
|
})());
|
|
10658
11392
|
if (existing) {
|
|
10659
|
-
result =
|
|
11393
|
+
result = path2.resolve(existing);
|
|
10660
11394
|
} else if (projectLibraryPath) {
|
|
10661
11395
|
try {
|
|
10662
11396
|
result = _AssetTools.ensureWritableDirectory(projectLibraryPath, "project-root");
|
|
@@ -10777,11 +11511,11 @@ var init_asset_tools = __esm({
|
|
|
10777
11511
|
if (response && response.success && response.buildData) {
|
|
10778
11512
|
const buildData = response.buildData;
|
|
10779
11513
|
const buildId = buildData.id || `${style}/exported`;
|
|
10780
|
-
const filePath =
|
|
10781
|
-
const dirPath =
|
|
10782
|
-
if (!
|
|
10783
|
-
|
|
10784
|
-
|
|
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));
|
|
10785
11519
|
response.savedTo = filePath;
|
|
10786
11520
|
}
|
|
10787
11521
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
@@ -10793,11 +11527,11 @@ var init_asset_tools = __esm({
|
|
|
10793
11527
|
const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
|
|
10794
11528
|
const computedBounds = bounds || this.computeBounds(normalizedParts);
|
|
10795
11529
|
const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
|
|
10796
|
-
const filePath =
|
|
10797
|
-
const dirPath =
|
|
10798
|
-
if (!
|
|
10799
|
-
|
|
10800
|
-
|
|
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));
|
|
10801
11535
|
return {
|
|
10802
11536
|
content: [{ type: "text", text: JSON.stringify({
|
|
10803
11537
|
success: true,
|
|
@@ -10822,11 +11556,11 @@ var init_asset_tools = __esm({
|
|
|
10822
11556
|
const buildData = { id, style, bounds: result.bounds, palette, parts: result.parts, generatorCode: code };
|
|
10823
11557
|
if (seed !== void 0)
|
|
10824
11558
|
buildData.generatorSeed = seed;
|
|
10825
|
-
const filePath =
|
|
10826
|
-
const dirPath =
|
|
10827
|
-
if (!
|
|
10828
|
-
|
|
10829
|
-
|
|
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));
|
|
10830
11564
|
return {
|
|
10831
11565
|
content: [{ type: "text", text: JSON.stringify({
|
|
10832
11566
|
success: true,
|
|
@@ -10840,11 +11574,11 @@ var init_asset_tools = __esm({
|
|
|
10840
11574
|
}
|
|
10841
11575
|
async importBuild(buildData, targetPath, position, instance_id) {
|
|
10842
11576
|
if (typeof buildData === "string") {
|
|
10843
|
-
const filePath =
|
|
10844
|
-
if (!
|
|
11577
|
+
const filePath = path2.join(_AssetTools.findLibraryPath(), `${buildData}.json`);
|
|
11578
|
+
if (!fs2.existsSync(filePath)) {
|
|
10845
11579
|
return toolErrorResult(`Build not found in library: ${buildData}`);
|
|
10846
11580
|
}
|
|
10847
|
-
buildData = JSON.parse(
|
|
11581
|
+
buildData = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
10848
11582
|
}
|
|
10849
11583
|
const response = await this.runtime.callSingle("/api/import-build", {
|
|
10850
11584
|
buildData,
|
|
@@ -10856,14 +11590,14 @@ var init_asset_tools = __esm({
|
|
|
10856
11590
|
async listLibrary(style) {
|
|
10857
11591
|
const libPath = _AssetTools.findLibraryPath();
|
|
10858
11592
|
let entries = [];
|
|
10859
|
-
if (
|
|
11593
|
+
if (fs2.existsSync(libPath)) {
|
|
10860
11594
|
const readDir = (dir) => {
|
|
10861
|
-
for (const entry of
|
|
11595
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
10862
11596
|
if (entry.isDirectory()) {
|
|
10863
|
-
readDir(
|
|
11597
|
+
readDir(path2.join(dir, entry.name));
|
|
10864
11598
|
} else if (entry.name.endsWith(".json")) {
|
|
10865
11599
|
try {
|
|
10866
|
-
const data = JSON.parse(
|
|
11600
|
+
const data = JSON.parse(fs2.readFileSync(path2.join(dir, entry.name), "utf-8"));
|
|
10867
11601
|
if (style && data.style !== style)
|
|
10868
11602
|
continue;
|
|
10869
11603
|
entries.push({
|
|
@@ -10888,10 +11622,10 @@ var init_asset_tools = __esm({
|
|
|
10888
11622
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
10889
11623
|
}
|
|
10890
11624
|
async getBuild(id) {
|
|
10891
|
-
const filePath =
|
|
10892
|
-
if (!
|
|
11625
|
+
const filePath = path2.join(_AssetTools.findLibraryPath(), `${id}.json`);
|
|
11626
|
+
if (!fs2.existsSync(filePath))
|
|
10893
11627
|
return toolErrorResult(`Build not found in library: ${id}`);
|
|
10894
|
-
const data = JSON.parse(
|
|
11628
|
+
const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
10895
11629
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
10896
11630
|
}
|
|
10897
11631
|
async importScene(sceneData, targetPath, instance_id) {
|
|
@@ -10935,10 +11669,10 @@ var init_asset_tools = __esm({
|
|
|
10935
11669
|
const buildId = models[modelKey];
|
|
10936
11670
|
if (!buildId)
|
|
10937
11671
|
throw new Error(`Unknown model key "${modelKey}" in scene placement`);
|
|
10938
|
-
const filePath =
|
|
10939
|
-
if (!
|
|
11672
|
+
const filePath = path2.join(_AssetTools.findLibraryPath(), `${buildId}.json`);
|
|
11673
|
+
if (!fs2.existsSync(filePath))
|
|
10940
11674
|
throw new Error(`Build "${buildId}" not found in library`);
|
|
10941
|
-
const buildData = JSON.parse(
|
|
11675
|
+
const buildData = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
10942
11676
|
await this.runtime.callSingle("/api/import-build", {
|
|
10943
11677
|
buildData,
|
|
10944
11678
|
targetPath: targetPath || "Workspace",
|
|
@@ -11104,10 +11838,10 @@ var init_asset_tools = __esm({
|
|
|
11104
11838
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
11105
11839
|
}
|
|
11106
11840
|
async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
|
|
11107
|
-
if (!
|
|
11841
|
+
if (!fs2.existsSync(filePath))
|
|
11108
11842
|
throw new Error(`File not found: ${filePath}`);
|
|
11109
|
-
const fileContent =
|
|
11110
|
-
const fileName =
|
|
11843
|
+
const fileContent = fs2.readFileSync(filePath);
|
|
11844
|
+
const fileName = path2.basename(filePath);
|
|
11111
11845
|
const cc = this.runtime.cookieClient;
|
|
11112
11846
|
const oc = this.runtime.openCloudClient;
|
|
11113
11847
|
if (assetType === "Decal" && cc.hasCookie()) {
|
|
@@ -11156,10 +11890,10 @@ var init_asset_tools = __esm({
|
|
|
11156
11890
|
if (!response.base64)
|
|
11157
11891
|
return toolErrorResult("plugin returned no base64 payload");
|
|
11158
11892
|
const bytes = Buffer.from(response.base64, "base64");
|
|
11159
|
-
const resolved =
|
|
11893
|
+
const resolved = path2.resolve(outputPath);
|
|
11160
11894
|
try {
|
|
11161
|
-
|
|
11162
|
-
|
|
11895
|
+
fs2.mkdirSync(path2.dirname(resolved), { recursive: true });
|
|
11896
|
+
fs2.writeFileSync(resolved, bytes);
|
|
11163
11897
|
} catch (err) {
|
|
11164
11898
|
return toolErrorResult(`failed to write ${resolved}: ${err.message}`);
|
|
11165
11899
|
}
|
|
@@ -11183,9 +11917,9 @@ var init_asset_tools = __esm({
|
|
|
11183
11917
|
let bytes;
|
|
11184
11918
|
let sourceLabel;
|
|
11185
11919
|
if (source.path !== void 0) {
|
|
11186
|
-
const resolved =
|
|
11920
|
+
const resolved = path2.resolve(source.path);
|
|
11187
11921
|
try {
|
|
11188
|
-
bytes =
|
|
11922
|
+
bytes = fs2.readFileSync(resolved);
|
|
11189
11923
|
} catch (err) {
|
|
11190
11924
|
return toolErrorResult(`failed to read ${resolved}: ${err.message}`);
|
|
11191
11925
|
}
|
|
@@ -11237,10 +11971,10 @@ var init_asset_tools = __esm({
|
|
|
11237
11971
|
const { buffer, contentType } = await this.runtime.imageClient.generate(prompt, options ?? {});
|
|
11238
11972
|
const ext = contentType.includes("png") ? "png" : "jpg";
|
|
11239
11973
|
const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "image";
|
|
11240
|
-
const dir =
|
|
11241
|
-
|
|
11242
|
-
const file =
|
|
11243
|
-
|
|
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);
|
|
11244
11978
|
return { file, bytes: buffer.length, model: options?.model ?? DEFAULT_IMAGE_MODEL };
|
|
11245
11979
|
}
|
|
11246
11980
|
async imageGenerate(prompt, options) {
|
|
@@ -11750,8 +12484,8 @@ var init_episode_reasoning = __esm({
|
|
|
11750
12484
|
});
|
|
11751
12485
|
|
|
11752
12486
|
// ../core/dist/tools/runtime-tools.js
|
|
11753
|
-
import * as
|
|
11754
|
-
import * as
|
|
12487
|
+
import * as fs3 from "fs";
|
|
12488
|
+
import * as path3 from "path";
|
|
11755
12489
|
function isEndTestAlreadyCalledError(value) {
|
|
11756
12490
|
return /EndTest.*can only be called once|can only be called once/i.test(errorMessage(value));
|
|
11757
12491
|
}
|
|
@@ -12636,9 +13370,9 @@ var init_runtime_tools = __esm({
|
|
|
12636
13370
|
const rawJson = mutable.raw_json;
|
|
12637
13371
|
if (typeof rawJson === "string") {
|
|
12638
13372
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
12639
|
-
const resolvedOutputPath =
|
|
12640
|
-
|
|
12641
|
-
|
|
13373
|
+
const resolvedOutputPath = path3.resolve(outputPath);
|
|
13374
|
+
fs3.mkdirSync(path3.dirname(resolvedOutputPath), { recursive: true });
|
|
13375
|
+
fs3.writeFileSync(resolvedOutputPath, rawJson, "utf8");
|
|
12642
13376
|
mutable.output_path = resolvedOutputPath;
|
|
12643
13377
|
}
|
|
12644
13378
|
delete mutable.raw_json;
|
|
@@ -13351,6 +14085,7 @@ function buildCreateSoundLuau(options) {
|
|
|
13351
14085
|
const lines = [
|
|
13352
14086
|
`local parent = resolvePath(${luaString(options.parentPath)})`,
|
|
13353
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",
|
|
13354
14089
|
`local sound = Instance.new("Sound")`,
|
|
13355
14090
|
`sound.SoundId = ${luaString(assetUri(options.soundId))}`
|
|
13356
14091
|
];
|
|
@@ -13371,6 +14106,7 @@ function buildCreateSoundLuau(options) {
|
|
|
13371
14106
|
function buildPlaySoundLuau(options) {
|
|
13372
14107
|
const body = `local sound = resolvePath(${luaString(options.path)})
|
|
13373
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
|
|
13374
14110
|
sound:Play()
|
|
13375
14111
|
return { path = sound:GetFullName(), playing = true, success = true }`;
|
|
13376
14112
|
return wrap(body);
|
|
@@ -13379,6 +14115,7 @@ function buildCreateAnimationLuau(options) {
|
|
|
13379
14115
|
const lines = [
|
|
13380
14116
|
`local parent = resolvePath(${luaString(options.parentPath)})`,
|
|
13381
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",
|
|
13382
14119
|
`local anim = Instance.new("Animation")`,
|
|
13383
14120
|
`anim.AnimationId = ${luaString(assetUri(options.animationId))}`,
|
|
13384
14121
|
`anim.Name = ${luaString(options.name ?? "Animation")}`,
|
|
@@ -13397,6 +14134,7 @@ if animator == nil then
|
|
|
13397
14134
|
animator = Instance.new("Animator")
|
|
13398
14135
|
animator.Parent = controller
|
|
13399
14136
|
end
|
|
14137
|
+
if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
|
|
13400
14138
|
local anim = Instance.new("Animation")
|
|
13401
14139
|
anim.AnimationId = ${luaString(assetUri(options.animationId))}
|
|
13402
14140
|
local track = animator:LoadAnimation(anim)
|
|
@@ -13410,6 +14148,7 @@ function buildApplyTextureLuau(options) {
|
|
|
13410
14148
|
if (options.property) {
|
|
13411
14149
|
const body2 = `local target = resolvePath(${luaString(options.targetPath)})
|
|
13412
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
|
|
13413
14152
|
target.${options.property.replace(/[^A-Za-z0-9]/g, "")} = ${uri}
|
|
13414
14153
|
return { path = target:GetFullName(), property = ${luaString(options.property)}, success = true }`;
|
|
13415
14154
|
return wrap(body2);
|
|
@@ -13417,6 +14156,7 @@ return { path = target:GetFullName(), property = ${luaString(options.property)},
|
|
|
13417
14156
|
const body = `local target = resolvePath(${luaString(options.targetPath)})
|
|
13418
14157
|
if target == nil then error("Target not found: " .. ${luaString(options.targetPath)}) end
|
|
13419
14158
|
local uri = ${uri}
|
|
14159
|
+
if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
|
|
13420
14160
|
local prop
|
|
13421
14161
|
if target:IsA("ImageLabel") or target:IsA("ImageButton") then
|
|
13422
14162
|
prop = "Image"
|
|
@@ -13456,6 +14196,7 @@ local parent = resolvePath(${luaString(parentPath)})
|
|
|
13456
14196
|
if parent == nil then error("Parent not found: " .. ${luaString(parentPath)}) end
|
|
13457
14197
|
local inputs = { ${inputs.join(", ")} }
|
|
13458
14198
|
local schema = ${schemaLua}
|
|
14199
|
+
if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
|
|
13459
14200
|
local model, meta = GenerationService:GenerateModelAsync(inputs, schema)
|
|
13460
14201
|
if model == nil then error("GenerateModelAsync returned no model") end
|
|
13461
14202
|
local desiredName = ${nameLua}
|
|
@@ -13520,6 +14261,7 @@ local function ensureCorner(o)
|
|
|
13520
14261
|
end
|
|
13521
14262
|
end
|
|
13522
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
|
|
13523
14265
|
if o:IsA("GuiButton") then
|
|
13524
14266
|
o.BackgroundColor3 = PRIMARY
|
|
13525
14267
|
o.BorderSizePixel = 0
|
|
@@ -13600,6 +14342,7 @@ end
|
|
|
13600
14342
|
local function lintRoot(root)
|
|
13601
14343
|
local interactives = {}
|
|
13602
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
|
|
13603
14346
|
if o:IsA("GuiObject") then
|
|
13604
14347
|
if (o:IsA("TextLabel") or o:IsA("TextButton") or o:IsA("TextBox")) and not o.TextScaled and o.TextSize < MIN_TEXT_SIZE then
|
|
13605
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))
|
|
@@ -13734,6 +14477,7 @@ function buildScreenGuiLuau(options) {
|
|
|
13734
14477
|
const parentPath = options.parentPath ?? "StarterGui";
|
|
13735
14478
|
const lines = [
|
|
13736
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",
|
|
13737
14481
|
`local obj = Instance.new("ScreenGui")`,
|
|
13738
14482
|
`obj.Name = ${luaString(options.name)}`
|
|
13739
14483
|
];
|
|
@@ -13751,6 +14495,7 @@ function buildGuiObjectLuau(className, options) {
|
|
|
13751
14495
|
const lines = [
|
|
13752
14496
|
`local parent = resolvePath(${luaString(options.parentPath)})`,
|
|
13753
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",
|
|
13754
14499
|
`local obj = Instance.new(${luaString(className)})`
|
|
13755
14500
|
];
|
|
13756
14501
|
if (options.name !== void 0)
|
|
@@ -13793,6 +14538,7 @@ function buildApplyLayoutLuau(targetPath, options) {
|
|
|
13793
14538
|
`local target = resolvePath(${luaString(targetPath)})`,
|
|
13794
14539
|
`if target == nil then error("Target not found: " .. ${luaString(targetPath)}) end`
|
|
13795
14540
|
];
|
|
14541
|
+
lines.push("if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end");
|
|
13796
14542
|
if (options.layout === "grid") {
|
|
13797
14543
|
lines.push(`local layout = Instance.new("UIGridLayout")`);
|
|
13798
14544
|
if (options.cellSize)
|
|
@@ -13826,6 +14572,7 @@ local function ensureScale(gui)
|
|
|
13826
14572
|
scale.Parent = gui
|
|
13827
14573
|
end
|
|
13828
14574
|
end
|
|
14575
|
+
if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end
|
|
13829
14576
|
local descendants = target:GetDescendants()
|
|
13830
14577
|
table.insert(descendants, target)
|
|
13831
14578
|
for _, child in ipairs(descendants) do
|
|
@@ -13858,7 +14605,10 @@ function clampClock(hour) {
|
|
|
13858
14605
|
return Math.max(0, Math.min(24, hour));
|
|
13859
14606
|
}
|
|
13860
14607
|
function buildSetTimeOfDayLuau(time) {
|
|
13861
|
-
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
|
+
];
|
|
13862
14612
|
if (typeof time === "number") {
|
|
13863
14613
|
lines.push(`Lighting.ClockTime = ${luaNumber(clampClock(time))}`);
|
|
13864
14614
|
} else {
|
|
@@ -13908,6 +14658,7 @@ function buildLightingPresetLuau(preset, withPostFx = false) {
|
|
|
13908
14658
|
}
|
|
13909
14659
|
const lines = [
|
|
13910
14660
|
'local Lighting = game:GetService("Lighting")',
|
|
14661
|
+
"if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
|
|
13911
14662
|
`Lighting.ClockTime = ${luaNumber(config.clockTime)}`,
|
|
13912
14663
|
`Lighting.Ambient = ${color3FromRGB(...config.ambient)}`,
|
|
13913
14664
|
`Lighting.OutdoorAmbient = ${color3FromRGB(...config.outdoorAmbient)}`,
|
|
@@ -13929,7 +14680,10 @@ function buildLightingPresetLuau(preset, withPostFx = false) {
|
|
|
13929
14680
|
return lines.join("\n");
|
|
13930
14681
|
}
|
|
13931
14682
|
function buildAtmosphereLuau(options) {
|
|
13932
|
-
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
|
+
];
|
|
13933
14687
|
lines.push(...atmosphereLines("Lighting", options));
|
|
13934
14688
|
lines.push("return { success = true }");
|
|
13935
14689
|
return lines.join("\n");
|
|
@@ -13937,6 +14691,7 @@ function buildAtmosphereLuau(options) {
|
|
|
13937
14691
|
function buildSkyLuau(options) {
|
|
13938
14692
|
const lines = [
|
|
13939
14693
|
'local Lighting = game:GetService("Lighting")',
|
|
14694
|
+
"if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
|
|
13940
14695
|
'local sky = Lighting:FindFirstChildOfClass("Sky") or Instance.new("Sky")'
|
|
13941
14696
|
];
|
|
13942
14697
|
if (options.sunTextureId !== void 0)
|
|
@@ -13970,6 +14725,7 @@ RunService.Heartbeat:Connect(function(dt)
|
|
|
13970
14725
|
end)`;
|
|
13971
14726
|
const lines = [
|
|
13972
14727
|
'local ServerScriptService = game:GetService("ServerScriptService")',
|
|
14728
|
+
"if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
|
|
13973
14729
|
`local existing = ServerScriptService:FindFirstChild(${luaString(scriptName)})`,
|
|
13974
14730
|
"if existing then existing:Destroy() end",
|
|
13975
14731
|
'local script = Instance.new("Script")',
|
|
@@ -14082,6 +14838,7 @@ function buildBaseplateLuau(options) {
|
|
|
14082
14838
|
const mat = material(options.material ?? "Grass");
|
|
14083
14839
|
return [
|
|
14084
14840
|
"local Terrain = workspace.Terrain",
|
|
14841
|
+
"if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
|
|
14085
14842
|
`Terrain:FillBlock(CFrame.new(${vector3(...pos)}), ${vector3(...options.size)}, ${mat})`,
|
|
14086
14843
|
`return { shape = "baseplate", volume = ${luaNumber(boxVolume(options.size))}, success = true }`
|
|
14087
14844
|
].join("\n");
|
|
@@ -14090,6 +14847,7 @@ function buildIslandLuau(options) {
|
|
|
14090
14847
|
const mat = material(options.material ?? "Sand");
|
|
14091
14848
|
const lines = [
|
|
14092
14849
|
"local Terrain = workspace.Terrain",
|
|
14850
|
+
"if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
|
|
14093
14851
|
`Terrain:FillBall(${vector3(...options.center)}, ${luaNumber(options.radius)}, ${mat})`
|
|
14094
14852
|
];
|
|
14095
14853
|
if (options.waterMaterial || options.waterRadius) {
|
|
@@ -14114,7 +14872,9 @@ function buildMountainsLuau(options) {
|
|
|
14114
14872
|
`local maxHeight = ${luaNumber(options.maxHeight)}`,
|
|
14115
14873
|
`local seed = ${luaNumber(seed)}`,
|
|
14116
14874
|
`local baseX, baseY, baseZ = ${luaNumber(cx - ex / 2)}, ${luaNumber(cy)}, ${luaNumber(cz - ez / 2)}`,
|
|
14117
|
-
`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",
|
|
14118
14878
|
` for gz = 0, ${luaNumber(ez)}, res do`,
|
|
14119
14879
|
" local wx = baseX + gx",
|
|
14120
14880
|
" local wz = baseZ + gz",
|
|
@@ -14130,6 +14890,7 @@ function buildWaterLuau(options) {
|
|
|
14130
14890
|
const pos = options.position ?? [0, 0, 0];
|
|
14131
14891
|
return [
|
|
14132
14892
|
"local Terrain = workspace.Terrain",
|
|
14893
|
+
"if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
|
|
14133
14894
|
`Terrain:FillBlock(CFrame.new(${vector3(...pos)}), ${vector3(...options.size)}, Enum.Material.Water)`,
|
|
14134
14895
|
`return { shape = "water", volume = ${luaNumber(boxVolume(options.size))}, success = true }`
|
|
14135
14896
|
].join("\n");
|
|
@@ -14140,6 +14901,7 @@ function buildPaintMaterialLuau(options) {
|
|
|
14140
14901
|
const op = options.replaceMaterial ? `Terrain:ReplaceMaterial(${region}, 4, ${material(options.replaceMaterial)}, ${target})` : `Terrain:FillRegion(${region}, 4, ${target})`;
|
|
14141
14902
|
return [
|
|
14142
14903
|
"local Terrain = workspace.Terrain",
|
|
14904
|
+
"if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
|
|
14143
14905
|
op,
|
|
14144
14906
|
'return { shape = "paint", success = true }'
|
|
14145
14907
|
].join("\n");
|
|
@@ -14147,6 +14909,7 @@ function buildPaintMaterialLuau(options) {
|
|
|
14147
14909
|
function buildClearRegionLuau(options) {
|
|
14148
14910
|
return [
|
|
14149
14911
|
"local Terrain = workspace.Terrain",
|
|
14912
|
+
"if _G.__mcp and _G.__mcp.checkCancelled and _G.__mcp.checkCancelled() then return { cancelled = true } end",
|
|
14150
14913
|
`Terrain:FillRegion(${region3(options.min, options.max)}, 4, Enum.Material.Air)`,
|
|
14151
14914
|
'return { shape = "clear", success = true }'
|
|
14152
14915
|
].join("\n");
|
|
@@ -14227,6 +14990,8 @@ spawn.Anchored = true
|
|
|
14227
14990
|
|
|
14228
14991
|
local checkpoints = ensure(course, "Checkpoints", "Folder")
|
|
14229
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
|
|
14230
14995
|
local cp = makePart(checkpoints, "Checkpoint" .. i, Vector3.new(10, 1, 10), Vector3.new(i * 40, 5, 0), Color3.fromRGB(80, 200, 120), true)
|
|
14231
14996
|
cp:SetAttribute("Stage", i)
|
|
14232
14997
|
if i > 0 then
|
|
@@ -14453,6 +15218,8 @@ local arena = ensure(Workspace, "Arena", "Model")
|
|
|
14453
15218
|
makePart(arena, "ArenaFloor", Vector3.new(120, 1, 120), Vector3.new(300, 5, 0), Color3.fromRGB(110, 110, 120), true)
|
|
14454
15219
|
local teleports = ensure(arena, "TeleportPoints", "Folder")
|
|
14455
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
|
|
14456
15223
|
local angle = (i / NUM_TELEPORTS) * math.pi * 2
|
|
14457
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)
|
|
14458
15225
|
tp.Transparency = 0.5
|
|
@@ -14719,8 +15486,8 @@ var init_sync_luau = __esm({
|
|
|
14719
15486
|
});
|
|
14720
15487
|
|
|
14721
15488
|
// ../core/dist/tools/sync-tools.js
|
|
14722
|
-
import * as
|
|
14723
|
-
import * as
|
|
15489
|
+
import * as fs4 from "fs";
|
|
15490
|
+
import * as path4 from "path";
|
|
14724
15491
|
var SyncTools;
|
|
14725
15492
|
var init_sync_tools = __esm({
|
|
14726
15493
|
"../core/dist/tools/sync-tools.js"() {
|
|
@@ -14740,11 +15507,11 @@ var init_sync_tools = __esm({
|
|
|
14740
15507
|
// clobbering. SyncManager owns the (tested) path/conflict logic; this layer
|
|
14741
15508
|
// owns filesystem and Studio I/O.
|
|
14742
15509
|
_syncManifestPath(dir) {
|
|
14743
|
-
return
|
|
15510
|
+
return path4.join(dir, ".robloxsync.json");
|
|
14744
15511
|
}
|
|
14745
15512
|
_readManifest(dir) {
|
|
14746
15513
|
try {
|
|
14747
|
-
const raw =
|
|
15514
|
+
const raw = fs4.readFileSync(this._syncManifestPath(dir), "utf8");
|
|
14748
15515
|
const parsed = JSON.parse(raw);
|
|
14749
15516
|
return parsed && typeof parsed.paths === "object" ? parsed.paths : {};
|
|
14750
15517
|
} catch {
|
|
@@ -14753,7 +15520,7 @@ var init_sync_tools = __esm({
|
|
|
14753
15520
|
}
|
|
14754
15521
|
_writeManifest(dir, paths) {
|
|
14755
15522
|
const payload = { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), paths };
|
|
14756
|
-
|
|
15523
|
+
fs4.writeFileSync(this._syncManifestPath(dir), JSON.stringify(payload, null, 2));
|
|
14757
15524
|
}
|
|
14758
15525
|
async _dumpStudioScripts(instance_id) {
|
|
14759
15526
|
const response = await this.runtime.callSingle("/api/execute-luau", { code: buildDumpScriptsLuau() }, "edit", instance_id);
|
|
@@ -14776,17 +15543,17 @@ var init_sync_tools = __esm({
|
|
|
14776
15543
|
const walk = (current) => {
|
|
14777
15544
|
let entries;
|
|
14778
15545
|
try {
|
|
14779
|
-
entries =
|
|
15546
|
+
entries = fs4.readdirSync(current, { withFileTypes: true });
|
|
14780
15547
|
} catch {
|
|
14781
15548
|
return;
|
|
14782
15549
|
}
|
|
14783
15550
|
for (const entry of entries) {
|
|
14784
|
-
const full =
|
|
14785
|
-
const rel =
|
|
15551
|
+
const full = path4.join(current, entry.name);
|
|
15552
|
+
const rel = path4.relative(dir, full).split(path4.sep).join("/");
|
|
14786
15553
|
if (entry.isDirectory()) {
|
|
14787
15554
|
walk(full);
|
|
14788
15555
|
} else if (this.sync.classNameForFile(entry.name) && !this.sync.isIgnored(rel)) {
|
|
14789
|
-
out.set(rel,
|
|
15556
|
+
out.set(rel, fs4.readFileSync(full, "utf8"));
|
|
14790
15557
|
}
|
|
14791
15558
|
}
|
|
14792
15559
|
};
|
|
@@ -14794,12 +15561,12 @@ var init_sync_tools = __esm({
|
|
|
14794
15561
|
return out;
|
|
14795
15562
|
}
|
|
14796
15563
|
_resolveSyncDir(syncDir) {
|
|
14797
|
-
return
|
|
15564
|
+
return path4.resolve(syncDir ?? process.env.ROBLOX_SYNC_DIR ?? path4.join(process.cwd(), "roblox-src"));
|
|
14798
15565
|
}
|
|
14799
15566
|
async syncPull(syncDir, instance_id) {
|
|
14800
15567
|
const dir = this._resolveSyncDir(syncDir);
|
|
14801
15568
|
const scripts = await this._dumpStudioScripts(instance_id);
|
|
14802
|
-
|
|
15569
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
14803
15570
|
const manifest = {};
|
|
14804
15571
|
let written = 0;
|
|
14805
15572
|
let skipped = 0;
|
|
@@ -14809,9 +15576,9 @@ var init_sync_tools = __esm({
|
|
|
14809
15576
|
skipped++;
|
|
14810
15577
|
continue;
|
|
14811
15578
|
}
|
|
14812
|
-
const full =
|
|
14813
|
-
|
|
14814
|
-
|
|
15579
|
+
const full = path4.join(dir, rel);
|
|
15580
|
+
fs4.mkdirSync(path4.dirname(full), { recursive: true });
|
|
15581
|
+
fs4.writeFileSync(full, script.source);
|
|
14815
15582
|
manifest[rel] = script.source;
|
|
14816
15583
|
written++;
|
|
14817
15584
|
}
|
|
@@ -15136,7 +15903,7 @@ var init_opencloud_client = __esm({
|
|
|
15136
15903
|
if (result.error) {
|
|
15137
15904
|
throw new Error(`Asset upload failed: ${result.error.message}`);
|
|
15138
15905
|
}
|
|
15139
|
-
await new Promise((
|
|
15906
|
+
await new Promise((resolve7) => setTimeout(resolve7, intervalMs));
|
|
15140
15907
|
}
|
|
15141
15908
|
throw new Error(`Asset upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
|
|
15142
15909
|
}
|
|
@@ -15145,9 +15912,9 @@ var init_opencloud_client = __esm({
|
|
|
15145
15912
|
});
|
|
15146
15913
|
|
|
15147
15914
|
// ../core/dist/managed-instance-registry.js
|
|
15148
|
-
import * as
|
|
15149
|
-
import * as
|
|
15150
|
-
import * as
|
|
15915
|
+
import * as fs5 from "fs";
|
|
15916
|
+
import * as os3 from "os";
|
|
15917
|
+
import * as path5 from "path";
|
|
15151
15918
|
function defaultManagedInstanceRegistryDir() {
|
|
15152
15919
|
if (process.env.BLOXFORGE_MANAGED_INSTANCE_REGISTRY_DIR) {
|
|
15153
15920
|
return process.env.BLOXFORGE_MANAGED_INSTANCE_REGISTRY_DIR;
|
|
@@ -15155,22 +15922,22 @@ function defaultManagedInstanceRegistryDir() {
|
|
|
15155
15922
|
if (process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR) {
|
|
15156
15923
|
return process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR;
|
|
15157
15924
|
}
|
|
15158
|
-
const suffix =
|
|
15925
|
+
const suffix = path5.join("managed-instances", "v1");
|
|
15159
15926
|
let newDir;
|
|
15160
15927
|
let legacyDir;
|
|
15161
15928
|
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
|
|
15162
|
-
newDir =
|
|
15163
|
-
legacyDir =
|
|
15929
|
+
newDir = path5.join(process.env.LOCALAPPDATA, "bloxforge", suffix);
|
|
15930
|
+
legacyDir = path5.join(process.env.LOCALAPPDATA, "robloxstudio-mcp", suffix);
|
|
15164
15931
|
} else if (process.platform === "darwin") {
|
|
15165
|
-
newDir =
|
|
15166
|
-
legacyDir =
|
|
15932
|
+
newDir = path5.join(os3.homedir(), "Library", "Application Support", "bloxforge", suffix);
|
|
15933
|
+
legacyDir = path5.join(os3.homedir(), "Library", "Application Support", "robloxstudio-mcp", suffix);
|
|
15167
15934
|
} else {
|
|
15168
|
-
newDir =
|
|
15169
|
-
legacyDir =
|
|
15935
|
+
newDir = path5.join(os3.homedir(), ".local", "state", "bloxforge", suffix);
|
|
15936
|
+
legacyDir = path5.join(os3.homedir(), ".local", "state", "robloxstudio-mcp", suffix);
|
|
15170
15937
|
}
|
|
15171
|
-
if (
|
|
15938
|
+
if (fs5.existsSync(newDir))
|
|
15172
15939
|
return newDir;
|
|
15173
|
-
if (
|
|
15940
|
+
if (fs5.existsSync(legacyDir))
|
|
15174
15941
|
return legacyDir;
|
|
15175
15942
|
return newDir;
|
|
15176
15943
|
}
|
|
@@ -15252,20 +16019,20 @@ var init_managed_instance_registry = __esm({
|
|
|
15252
16019
|
}
|
|
15253
16020
|
withLock(fn) {
|
|
15254
16021
|
this.ensureDir();
|
|
15255
|
-
const lockDir =
|
|
16022
|
+
const lockDir = path5.join(this.dir, ".lock");
|
|
15256
16023
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
15257
16024
|
while (true) {
|
|
15258
16025
|
try {
|
|
15259
|
-
|
|
16026
|
+
fs5.mkdirSync(lockDir);
|
|
15260
16027
|
break;
|
|
15261
16028
|
} catch (error) {
|
|
15262
16029
|
const code = error.code;
|
|
15263
16030
|
if (code !== "EEXIST")
|
|
15264
16031
|
throw error;
|
|
15265
16032
|
try {
|
|
15266
|
-
const stat =
|
|
16033
|
+
const stat = fs5.statSync(lockDir);
|
|
15267
16034
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
15268
|
-
|
|
16035
|
+
fs5.rmSync(lockDir, { recursive: true, force: true });
|
|
15269
16036
|
continue;
|
|
15270
16037
|
}
|
|
15271
16038
|
} catch {
|
|
@@ -15280,21 +16047,21 @@ var init_managed_instance_registry = __esm({
|
|
|
15280
16047
|
try {
|
|
15281
16048
|
return fn();
|
|
15282
16049
|
} finally {
|
|
15283
|
-
|
|
16050
|
+
fs5.rmSync(lockDir, { recursive: true, force: true });
|
|
15284
16051
|
}
|
|
15285
16052
|
}
|
|
15286
16053
|
ensureDir() {
|
|
15287
|
-
|
|
16054
|
+
fs5.mkdirSync(this.dir, { recursive: true });
|
|
15288
16055
|
}
|
|
15289
16056
|
recordPath(recordId) {
|
|
15290
|
-
return
|
|
16057
|
+
return path5.join(this.dir, `${recordId}.json`);
|
|
15291
16058
|
}
|
|
15292
16059
|
recordFilesUnlocked() {
|
|
15293
|
-
return
|
|
16060
|
+
return fs5.readdirSync(this.dir).filter((name) => name.endsWith(".json")).map((name) => path5.join(this.dir, name));
|
|
15294
16061
|
}
|
|
15295
16062
|
readRecordUnlocked(recordId) {
|
|
15296
16063
|
try {
|
|
15297
|
-
const parsed = JSON.parse(
|
|
16064
|
+
const parsed = JSON.parse(fs5.readFileSync(this.recordPath(recordId), "utf8"));
|
|
15298
16065
|
return isRecord(parsed) ? parsed : void 0;
|
|
15299
16066
|
} catch {
|
|
15300
16067
|
return void 0;
|
|
@@ -15307,7 +16074,7 @@ var init_managed_instance_registry = __esm({
|
|
|
15307
16074
|
const records = [];
|
|
15308
16075
|
for (const file of this.recordFilesUnlocked()) {
|
|
15309
16076
|
try {
|
|
15310
|
-
const parsed = JSON.parse(
|
|
16077
|
+
const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
15311
16078
|
if (!isRecord(parsed))
|
|
15312
16079
|
continue;
|
|
15313
16080
|
records.push(parsed);
|
|
@@ -15319,23 +16086,23 @@ var init_managed_instance_registry = __esm({
|
|
|
15319
16086
|
writeRecordUnlocked(record) {
|
|
15320
16087
|
this.ensureDir();
|
|
15321
16088
|
const finalPath = this.recordPath(record.recordId);
|
|
15322
|
-
const tmpPath =
|
|
15323
|
-
const fd =
|
|
16089
|
+
const tmpPath = path5.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
|
|
16090
|
+
const fd = fs5.openSync(tmpPath, "w");
|
|
15324
16091
|
try {
|
|
15325
|
-
|
|
16092
|
+
fs5.writeFileSync(fd, `${JSON.stringify(record, null, 2)}
|
|
15326
16093
|
`, "utf8");
|
|
15327
|
-
|
|
16094
|
+
fs5.fsyncSync(fd);
|
|
15328
16095
|
} finally {
|
|
15329
|
-
|
|
16096
|
+
fs5.closeSync(fd);
|
|
15330
16097
|
}
|
|
15331
|
-
|
|
16098
|
+
fs5.renameSync(tmpPath, finalPath);
|
|
15332
16099
|
}
|
|
15333
16100
|
deleteRecordUnlocked(recordId) {
|
|
15334
|
-
|
|
16101
|
+
fs5.rmSync(this.recordPath(recordId), { force: true });
|
|
15335
16102
|
}
|
|
15336
16103
|
appendEventUnlocked(event, now) {
|
|
15337
|
-
const file =
|
|
15338
|
-
|
|
16104
|
+
const file = path5.join(this.dir, `events-${ymd(now)}.jsonl`);
|
|
16105
|
+
fs5.appendFileSync(file, `${JSON.stringify({
|
|
15339
16106
|
ts: new Date(now).toISOString(),
|
|
15340
16107
|
...event
|
|
15341
16108
|
})}
|
|
@@ -15343,12 +16110,12 @@ var init_managed_instance_registry = __esm({
|
|
|
15343
16110
|
}
|
|
15344
16111
|
cleanupOldEventLogsUnlocked(now) {
|
|
15345
16112
|
const cutoff = ymd(now - EVENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
|
|
15346
|
-
for (const name of
|
|
16113
|
+
for (const name of fs5.readdirSync(this.dir)) {
|
|
15347
16114
|
const date = eventLogDate(name);
|
|
15348
16115
|
if (!date || date >= cutoff)
|
|
15349
16116
|
continue;
|
|
15350
16117
|
try {
|
|
15351
|
-
|
|
16118
|
+
fs5.rmSync(path5.join(this.dir, name), { force: true });
|
|
15352
16119
|
} catch {
|
|
15353
16120
|
}
|
|
15354
16121
|
}
|
|
@@ -15372,9 +16139,9 @@ var init_managed_instance_registry = __esm({
|
|
|
15372
16139
|
for (const file of this.recordFilesUnlocked()) {
|
|
15373
16140
|
let parsed;
|
|
15374
16141
|
try {
|
|
15375
|
-
parsed = JSON.parse(
|
|
16142
|
+
parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
15376
16143
|
} catch {
|
|
15377
|
-
|
|
16144
|
+
fs5.rmSync(file, { force: true });
|
|
15378
16145
|
this.appendEventUnlocked({
|
|
15379
16146
|
event: "registry_pruned_malformed_record",
|
|
15380
16147
|
reason: "parse_error",
|
|
@@ -15383,7 +16150,7 @@ var init_managed_instance_registry = __esm({
|
|
|
15383
16150
|
continue;
|
|
15384
16151
|
}
|
|
15385
16152
|
if (!parsed || typeof parsed !== "object") {
|
|
15386
|
-
|
|
16153
|
+
fs5.rmSync(file, { force: true });
|
|
15387
16154
|
this.appendEventUnlocked({
|
|
15388
16155
|
event: "registry_pruned_malformed_record",
|
|
15389
16156
|
reason: "invalid_shape",
|
|
@@ -15395,7 +16162,7 @@ var init_managed_instance_registry = __esm({
|
|
|
15395
16162
|
if (typeof version === "number" && version > REGISTRY_VERSION)
|
|
15396
16163
|
continue;
|
|
15397
16164
|
if (!isRecord(parsed)) {
|
|
15398
|
-
|
|
16165
|
+
fs5.rmSync(file, { force: true });
|
|
15399
16166
|
this.appendEventUnlocked({
|
|
15400
16167
|
event: "registry_pruned_malformed_record",
|
|
15401
16168
|
reason: "invalid_shape",
|
|
@@ -15404,7 +16171,7 @@ var init_managed_instance_registry = __esm({
|
|
|
15404
16171
|
continue;
|
|
15405
16172
|
}
|
|
15406
16173
|
if (parsed.closedAt !== void 0) {
|
|
15407
|
-
|
|
16174
|
+
fs5.rmSync(file, { force: true });
|
|
15408
16175
|
this.appendEventUnlocked({
|
|
15409
16176
|
event: "registry_pruned_closed_record",
|
|
15410
16177
|
recordId: parsed.recordId,
|
|
@@ -15417,7 +16184,7 @@ var init_managed_instance_registry = __esm({
|
|
|
15417
16184
|
}
|
|
15418
16185
|
if (parsed.bootId !== options.currentBootId) {
|
|
15419
16186
|
this.cleanupRecord(options, parsed);
|
|
15420
|
-
|
|
16187
|
+
fs5.rmSync(file, { force: true });
|
|
15421
16188
|
this.appendEventUnlocked({
|
|
15422
16189
|
event: "registry_pruned_previous_boot",
|
|
15423
16190
|
recordId: parsed.recordId,
|
|
@@ -15430,7 +16197,7 @@ var init_managed_instance_registry = __esm({
|
|
|
15430
16197
|
}
|
|
15431
16198
|
if (options.isProcessRunning && (parsed.nativeProcessId || parsed.spawnPid) && !options.isProcessRunning(parsed)) {
|
|
15432
16199
|
this.cleanupRecord(options, parsed);
|
|
15433
|
-
|
|
16200
|
+
fs5.rmSync(file, { force: true });
|
|
15434
16201
|
this.appendEventUnlocked({
|
|
15435
16202
|
event: "registry_pruned_stale_process",
|
|
15436
16203
|
recordId: parsed.recordId,
|
|
@@ -15448,10 +16215,10 @@ var init_managed_instance_registry = __esm({
|
|
|
15448
16215
|
|
|
15449
16216
|
// ../core/dist/studio-instance-manager.js
|
|
15450
16217
|
import { execFileSync, spawn } from "child_process";
|
|
15451
|
-
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";
|
|
15452
16219
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
15453
|
-
import * as
|
|
15454
|
-
import * as
|
|
16220
|
+
import * as os4 from "os";
|
|
16221
|
+
import * as path6 from "path";
|
|
15455
16222
|
function run(command, args, options = {}) {
|
|
15456
16223
|
return execFileSync(command, args, {
|
|
15457
16224
|
encoding: "utf8",
|
|
@@ -15463,14 +16230,14 @@ function isWsl() {
|
|
|
15463
16230
|
if (process.platform !== "linux")
|
|
15464
16231
|
return false;
|
|
15465
16232
|
try {
|
|
15466
|
-
return /microsoft|wsl/i.test(
|
|
16233
|
+
return /microsoft|wsl/i.test(readFileSync5("/proc/version", "utf8"));
|
|
15467
16234
|
} catch {
|
|
15468
16235
|
return false;
|
|
15469
16236
|
}
|
|
15470
16237
|
}
|
|
15471
16238
|
function powershell(script) {
|
|
15472
16239
|
return run("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
15473
|
-
cwd: isWsl() &&
|
|
16240
|
+
cwd: isWsl() && existsSync4("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
15474
16241
|
});
|
|
15475
16242
|
}
|
|
15476
16243
|
function windowsLocalAppData() {
|
|
@@ -15480,7 +16247,7 @@ function windowsLocalAppData() {
|
|
|
15480
16247
|
return void 0;
|
|
15481
16248
|
try {
|
|
15482
16249
|
return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
|
|
15483
|
-
cwd:
|
|
16250
|
+
cwd: existsSync4("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
15484
16251
|
});
|
|
15485
16252
|
} catch {
|
|
15486
16253
|
return void 0;
|
|
@@ -15492,7 +16259,7 @@ function toWslPath(windowsPath) {
|
|
|
15492
16259
|
return run("wslpath", ["-u", windowsPath]);
|
|
15493
16260
|
}
|
|
15494
16261
|
function toStudioLaunchArg(arg) {
|
|
15495
|
-
if (!isWsl() || !
|
|
16262
|
+
if (!isWsl() || !path6.isAbsolute(arg) || !existsSync4(arg))
|
|
15496
16263
|
return arg;
|
|
15497
16264
|
return run("wslpath", ["-w", arg]);
|
|
15498
16265
|
}
|
|
@@ -15501,24 +16268,24 @@ function resolveEntrypointDir() {
|
|
|
15501
16268
|
if (!entrypoint)
|
|
15502
16269
|
return void 0;
|
|
15503
16270
|
try {
|
|
15504
|
-
return
|
|
16271
|
+
return path6.dirname(realpathSync(entrypoint));
|
|
15505
16272
|
} catch {
|
|
15506
|
-
return
|
|
16273
|
+
return path6.dirname(path6.resolve(entrypoint));
|
|
15507
16274
|
}
|
|
15508
16275
|
}
|
|
15509
16276
|
function resolveBaseplateTemplatePath() {
|
|
15510
16277
|
const entrypointDir = resolveEntrypointDir();
|
|
15511
16278
|
const candidates = [
|
|
15512
16279
|
...entrypointDir ? [
|
|
15513
|
-
|
|
15514
|
-
|
|
16280
|
+
path6.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
|
|
16281
|
+
path6.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
|
|
15515
16282
|
] : [],
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
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)
|
|
15519
16286
|
];
|
|
15520
16287
|
for (const candidate of candidates) {
|
|
15521
|
-
if (
|
|
16288
|
+
if (existsSync4(candidate))
|
|
15522
16289
|
return candidate;
|
|
15523
16290
|
}
|
|
15524
16291
|
throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
|
|
@@ -15545,7 +16312,7 @@ function sweepStaleBaseplateFiles() {
|
|
|
15545
16312
|
continue;
|
|
15546
16313
|
if (Number(match[1]) !== process.pid && isProcessAlive(Number(match[1])))
|
|
15547
16314
|
continue;
|
|
15548
|
-
const file =
|
|
16315
|
+
const file = path6.join(BASEPLATE_TEMP_DIR, entry);
|
|
15549
16316
|
try {
|
|
15550
16317
|
if (statSync3(file).mtimeMs < cutoff)
|
|
15551
16318
|
rmSync2(file, { force: true });
|
|
@@ -15554,15 +16321,15 @@ function sweepStaleBaseplateFiles() {
|
|
|
15554
16321
|
}
|
|
15555
16322
|
}
|
|
15556
16323
|
function createBaseplatePlaceFile() {
|
|
15557
|
-
|
|
16324
|
+
mkdirSync6(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
15558
16325
|
sweepStaleBaseplateFiles();
|
|
15559
|
-
const file =
|
|
15560
|
-
|
|
16326
|
+
const file = path6.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
|
|
16327
|
+
copyFileSync2(resolveBaseplateTemplatePath(), file);
|
|
15561
16328
|
return file;
|
|
15562
16329
|
}
|
|
15563
16330
|
function isGeneratedBaseplatePlaceFile(file) {
|
|
15564
|
-
const resolvedFile =
|
|
15565
|
-
return
|
|
16331
|
+
const resolvedFile = path6.resolve(file);
|
|
16332
|
+
return path6.dirname(resolvedFile) === path6.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path6.basename(resolvedFile));
|
|
15566
16333
|
}
|
|
15567
16334
|
function cleanupManagedBaseplateFiles(record) {
|
|
15568
16335
|
if (record.source !== "baseplate" || !record.localPlaceFile)
|
|
@@ -15594,11 +16361,11 @@ function resolveStudioExe() {
|
|
|
15594
16361
|
throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
|
|
15595
16362
|
}
|
|
15596
16363
|
const localAppData = windowsLocalAppData();
|
|
15597
|
-
const root = localAppData ?
|
|
15598
|
-
if (!
|
|
16364
|
+
const root = localAppData ? path6.join(toWslPath(localAppData), "Roblox", "Versions") : path6.join(os4.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
16365
|
+
if (!existsSync4(root)) {
|
|
15599
16366
|
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
15600
16367
|
}
|
|
15601
|
-
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);
|
|
15602
16369
|
if (candidates.length === 0) {
|
|
15603
16370
|
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
15604
16371
|
}
|
|
@@ -15633,7 +16400,7 @@ function listStudioProcesses() {
|
|
|
15633
16400
|
function currentBootId() {
|
|
15634
16401
|
if (process.platform === "linux") {
|
|
15635
16402
|
try {
|
|
15636
|
-
return
|
|
16403
|
+
return readFileSync5("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
15637
16404
|
} catch {
|
|
15638
16405
|
}
|
|
15639
16406
|
}
|
|
@@ -15649,7 +16416,7 @@ function currentBootId() {
|
|
|
15649
16416
|
} catch {
|
|
15650
16417
|
}
|
|
15651
16418
|
}
|
|
15652
|
-
return `${process.platform}:${
|
|
16419
|
+
return `${process.platform}:${os4.hostname()}:unknown-boot`;
|
|
15653
16420
|
}
|
|
15654
16421
|
function buildStudioLaunchArgs(options) {
|
|
15655
16422
|
switch (options.source) {
|
|
@@ -15685,17 +16452,17 @@ function buildStudioLaunchArgs(options) {
|
|
|
15685
16452
|
}
|
|
15686
16453
|
}
|
|
15687
16454
|
function delay(ms) {
|
|
15688
|
-
return new Promise((
|
|
16455
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
15689
16456
|
}
|
|
15690
16457
|
function basenameAny(filePath) {
|
|
15691
|
-
return
|
|
16458
|
+
return path6.basename(filePath.replace(/\\/g, "/"));
|
|
15692
16459
|
}
|
|
15693
16460
|
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
|
|
15694
16461
|
var init_studio_instance_manager = __esm({
|
|
15695
16462
|
"../core/dist/studio-instance-manager.js"() {
|
|
15696
16463
|
"use strict";
|
|
15697
16464
|
init_managed_instance_registry();
|
|
15698
|
-
BASEPLATE_TEMP_DIR =
|
|
16465
|
+
BASEPLATE_TEMP_DIR = path6.join(os4.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
15699
16466
|
BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
15700
16467
|
BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
15701
16468
|
STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -15744,7 +16511,7 @@ var init_studio_instance_manager = __esm({
|
|
|
15744
16511
|
const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
|
|
15745
16512
|
const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
|
|
15746
16513
|
const spawnOptions = {
|
|
15747
|
-
cwd: isWsl() &&
|
|
16514
|
+
cwd: isWsl() && existsSync4("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
15748
16515
|
detached: true,
|
|
15749
16516
|
stdio: "ignore"
|
|
15750
16517
|
};
|
|
@@ -15950,13 +16717,13 @@ var init_studio_instance_manager = __esm({
|
|
|
15950
16717
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
15951
16718
|
if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
|
|
15952
16719
|
return true;
|
|
15953
|
-
const processPath = studioProcess.Path ?
|
|
15954
|
-
const exePath = record.exe ?
|
|
16720
|
+
const processPath = studioProcess.Path ? path6.normalize(studioProcess.Path).toLowerCase() : "";
|
|
16721
|
+
const exePath = record.exe ? path6.normalize(record.exe).toLowerCase() : "";
|
|
15955
16722
|
if (processPath && exePath && (processPath === exePath || basenameAny(processPath) === basenameAny(exePath))) {
|
|
15956
16723
|
return true;
|
|
15957
16724
|
}
|
|
15958
16725
|
const commandLine = studioProcess.CommandLine ?? "";
|
|
15959
|
-
if (record.localPlaceFile && commandLine.includes(
|
|
16726
|
+
if (record.localPlaceFile && commandLine.includes(path6.basename(record.localPlaceFile)))
|
|
15960
16727
|
return true;
|
|
15961
16728
|
if (record.placeId !== void 0 && commandLine.includes(String(record.placeId)))
|
|
15962
16729
|
return true;
|
|
@@ -16157,10 +16924,207 @@ var init_session_recorder = __esm({
|
|
|
16157
16924
|
}
|
|
16158
16925
|
});
|
|
16159
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
|
+
|
|
16160
17124
|
// ../core/dist/tools/index.js
|
|
16161
|
-
import * as
|
|
16162
|
-
import * as
|
|
16163
|
-
import * as
|
|
17125
|
+
import * as fs7 from "fs";
|
|
17126
|
+
import * as os6 from "os";
|
|
17127
|
+
import * as path8 from "path";
|
|
16164
17128
|
import * as crypto from "crypto";
|
|
16165
17129
|
function requiresAttribution(license) {
|
|
16166
17130
|
return /cc[\s-]?by|attribution/i.test(license ?? "");
|
|
@@ -16220,8 +17184,8 @@ function loadMicroProfilerBaseline(source, sourcePath) {
|
|
|
16220
17184
|
if (sourcePath !== void 0) {
|
|
16221
17185
|
if (typeof sourcePath !== "string" || sourcePath === "")
|
|
16222
17186
|
throw new Error("baseline_path must be a non-empty string when provided");
|
|
16223
|
-
const resolved =
|
|
16224
|
-
const parsed = JSON.parse(
|
|
17187
|
+
const resolved = path8.resolve(sourcePath);
|
|
17188
|
+
const parsed = JSON.parse(fs7.readFileSync(resolved, "utf8"));
|
|
16225
17189
|
const record = asRecord(parsed);
|
|
16226
17190
|
if (!record)
|
|
16227
17191
|
throw new Error(`baseline_path did not contain a JSON object: ${resolved}`);
|
|
@@ -16353,6 +17317,7 @@ var init_tools = __esm({
|
|
|
16353
17317
|
init_roblox_cookie_client();
|
|
16354
17318
|
init_roblox_docs();
|
|
16355
17319
|
init_session_recorder();
|
|
17320
|
+
init_quality_tools();
|
|
16356
17321
|
init_runtime_support();
|
|
16357
17322
|
RobloxStudioTools = class _RobloxStudioTools {
|
|
16358
17323
|
client;
|
|
@@ -16375,6 +17340,7 @@ var init_tools = __esm({
|
|
|
16375
17340
|
runtimeTools;
|
|
16376
17341
|
episodes;
|
|
16377
17342
|
sessionRecorder;
|
|
17343
|
+
qualityTools;
|
|
16378
17344
|
/** Provenance for externally-imported assets (Track A) — source/license/hash/assetId. */
|
|
16379
17345
|
provenance = /* @__PURE__ */ new Map();
|
|
16380
17346
|
constructor(bridge) {
|
|
@@ -16412,7 +17378,7 @@ var init_tools = __esm({
|
|
|
16412
17378
|
this.scriptTools = new ScriptTools({
|
|
16413
17379
|
callSingle: this._callSingle.bind(this),
|
|
16414
17380
|
safetyGate: this._safetyGate.bind(this),
|
|
16415
|
-
backupScript: (
|
|
17381
|
+
backupScript: (path9, source) => this.safety.backupScript(path9, source),
|
|
16416
17382
|
recordOperation: (kind, summary) => this.safety.recordOperation({ kind, summary })
|
|
16417
17383
|
});
|
|
16418
17384
|
this.mutationTools = new MutationTools({
|
|
@@ -16431,6 +17397,7 @@ var init_tools = __esm({
|
|
|
16431
17397
|
});
|
|
16432
17398
|
this.episodes = new EpisodeStore();
|
|
16433
17399
|
this.sessionRecorder = new SessionRecorder();
|
|
17400
|
+
this.qualityTools = new QualityTools();
|
|
16434
17401
|
this.runtimeTools = new RuntimeTools({
|
|
16435
17402
|
bridge: this.bridge,
|
|
16436
17403
|
client: this.client,
|
|
@@ -16451,6 +17418,53 @@ var init_tools = __esm({
|
|
|
16451
17418
|
async getSessionSummary() {
|
|
16452
17419
|
return wrapToolJsonText(this.sessionRecorder.summarize());
|
|
16453
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
|
+
}
|
|
16454
17468
|
async getRobloxDocs(name, docType, section) {
|
|
16455
17469
|
if (!name || typeof name !== "string") {
|
|
16456
17470
|
throw new Error('get_roblox_docs requires a name (e.g. "ProximityPrompt")');
|
|
@@ -16628,8 +17642,8 @@ var init_tools = __esm({
|
|
|
16628
17642
|
}
|
|
16629
17643
|
// Scene-read inspection tools live in SceneReadTools; the facade delegates with
|
|
16630
17644
|
// identical signatures.
|
|
16631
|
-
async getFileTree(
|
|
16632
|
-
return this.sceneReadTools.getFileTree(
|
|
17645
|
+
async getFileTree(path9 = "", instance_id) {
|
|
17646
|
+
return this.sceneReadTools.getFileTree(path9, instance_id);
|
|
16633
17647
|
}
|
|
16634
17648
|
async searchFiles(query, searchType = "name", instance_id) {
|
|
16635
17649
|
const response = await this._callSingle("/api/search-files", { query, searchType }, void 0, instance_id);
|
|
@@ -16663,8 +17677,8 @@ var init_tools = __esm({
|
|
|
16663
17677
|
async getClassInfo(className, instance_id) {
|
|
16664
17678
|
return this.sceneReadTools.getClassInfo(className, instance_id);
|
|
16665
17679
|
}
|
|
16666
|
-
async getProjectStructure(
|
|
16667
|
-
return this.sceneReadTools.getProjectStructure(
|
|
17680
|
+
async getProjectStructure(path9, maxDepth, scriptsOnly, instance_id) {
|
|
17681
|
+
return this.sceneReadTools.getProjectStructure(path9, maxDepth, scriptsOnly, instance_id);
|
|
16668
17682
|
}
|
|
16669
17683
|
// Mutation tools live in MutationTools; the facade delegates with identical signatures.
|
|
16670
17684
|
async setProperty(instancePath, propertyName, propertyValue, instance_id) {
|
|
@@ -16848,14 +17862,14 @@ var init_tools = __esm({
|
|
|
16848
17862
|
return this.runtimeTools.redo(instance_id);
|
|
16849
17863
|
}
|
|
16850
17864
|
static findProjectRoot(startDir) {
|
|
16851
|
-
let dir =
|
|
17865
|
+
let dir = path8.resolve(startDir);
|
|
16852
17866
|
let previous = "";
|
|
16853
17867
|
while (dir !== previous) {
|
|
16854
|
-
if (
|
|
17868
|
+
if (fs7.existsSync(path8.join(dir, ".git")) || fs7.existsSync(path8.join(dir, "package.json"))) {
|
|
16855
17869
|
return dir;
|
|
16856
17870
|
}
|
|
16857
17871
|
previous = dir;
|
|
16858
|
-
dir =
|
|
17872
|
+
dir = path8.dirname(dir);
|
|
16859
17873
|
}
|
|
16860
17874
|
return null;
|
|
16861
17875
|
}
|
|
@@ -16863,15 +17877,15 @@ var init_tools = __esm({
|
|
|
16863
17877
|
if (!candidate)
|
|
16864
17878
|
return false;
|
|
16865
17879
|
try {
|
|
16866
|
-
return
|
|
17880
|
+
return fs7.statSync(candidate).isDirectory();
|
|
16867
17881
|
} catch {
|
|
16868
17882
|
return false;
|
|
16869
17883
|
}
|
|
16870
17884
|
}
|
|
16871
17885
|
static ensureWritableDirectory(candidate, label) {
|
|
16872
|
-
const resolved =
|
|
17886
|
+
const resolved = path8.resolve(candidate);
|
|
16873
17887
|
try {
|
|
16874
|
-
|
|
17888
|
+
fs7.mkdirSync(resolved, { recursive: true });
|
|
16875
17889
|
} catch (error) {
|
|
16876
17890
|
throw new Error(`Unable to create ${label} build-library directory at ${resolved}: ${error.message}`);
|
|
16877
17891
|
}
|
|
@@ -16879,7 +17893,7 @@ var init_tools = __esm({
|
|
|
16879
17893
|
throw new Error(`${label} build-library path is not a directory: ${resolved}`);
|
|
16880
17894
|
}
|
|
16881
17895
|
try {
|
|
16882
|
-
|
|
17896
|
+
fs7.accessSync(resolved, fs7.constants.W_OK);
|
|
16883
17897
|
} catch (error) {
|
|
16884
17898
|
throw new Error(`${label} build-library directory is not writable: ${resolved}. ${error.message}`);
|
|
16885
17899
|
}
|
|
@@ -16890,27 +17904,27 @@ var init_tools = __esm({
|
|
|
16890
17904
|
if (_RobloxStudioTools._cachedLibraryPath)
|
|
16891
17905
|
return _RobloxStudioTools._cachedLibraryPath;
|
|
16892
17906
|
const overridePath = process.env.BLOXFORGE_BUILD_LIBRARY || process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
|
|
16893
|
-
const cwd =
|
|
17907
|
+
const cwd = path8.resolve(process.cwd());
|
|
16894
17908
|
const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
|
|
16895
|
-
const newHomeLibraryPath =
|
|
16896
|
-
const legacyHomeLibraryPath =
|
|
16897
|
-
const homeLibraryPath =
|
|
16898
|
-
const projectLibraryPath = projectRoot ?
|
|
16899
|
-
const cwdLibraryPath =
|
|
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");
|
|
16900
17914
|
let result;
|
|
16901
17915
|
if (overridePath) {
|
|
16902
17916
|
result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
|
|
16903
17917
|
} else {
|
|
16904
17918
|
const existing = [projectLibraryPath, cwdLibraryPath].find((c) => c && _RobloxStudioTools.isDirectory(c) && (() => {
|
|
16905
17919
|
try {
|
|
16906
|
-
|
|
17920
|
+
fs7.accessSync(c, fs7.constants.W_OK);
|
|
16907
17921
|
return true;
|
|
16908
17922
|
} catch {
|
|
16909
17923
|
return false;
|
|
16910
17924
|
}
|
|
16911
17925
|
})());
|
|
16912
17926
|
if (existing) {
|
|
16913
|
-
result =
|
|
17927
|
+
result = path8.resolve(existing);
|
|
16914
17928
|
} else if (projectLibraryPath) {
|
|
16915
17929
|
try {
|
|
16916
17930
|
result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
|
|
@@ -16937,12 +17951,12 @@ var init_tools = __esm({
|
|
|
16937
17951
|
if (response && response.success && response.buildData) {
|
|
16938
17952
|
const buildData = response.buildData;
|
|
16939
17953
|
const buildId = buildData.id || `${style}/exported`;
|
|
16940
|
-
const filePath =
|
|
16941
|
-
const dirPath =
|
|
16942
|
-
if (!
|
|
16943
|
-
|
|
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 });
|
|
16944
17958
|
}
|
|
16945
|
-
|
|
17959
|
+
fs7.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
|
|
16946
17960
|
response.savedTo = filePath;
|
|
16947
17961
|
}
|
|
16948
17962
|
return {
|
|
@@ -17039,12 +18053,12 @@ var init_tools = __esm({
|
|
|
17039
18053
|
const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
|
|
17040
18054
|
const computedBounds = bounds || this.computeBounds(normalizedParts);
|
|
17041
18055
|
const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
|
|
17042
|
-
const filePath =
|
|
17043
|
-
const dirPath =
|
|
17044
|
-
if (!
|
|
17045
|
-
|
|
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 });
|
|
17046
18060
|
}
|
|
17047
|
-
|
|
18061
|
+
fs7.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
|
|
17048
18062
|
return {
|
|
17049
18063
|
content: [
|
|
17050
18064
|
{
|
|
@@ -17098,12 +18112,12 @@ var init_tools = __esm({
|
|
|
17098
18112
|
};
|
|
17099
18113
|
if (seed !== void 0)
|
|
17100
18114
|
buildData.generatorSeed = seed;
|
|
17101
|
-
const filePath =
|
|
17102
|
-
const dirPath =
|
|
17103
|
-
if (!
|
|
17104
|
-
|
|
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 });
|
|
17105
18119
|
}
|
|
17106
|
-
|
|
18120
|
+
fs7.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
|
|
17107
18121
|
return {
|
|
17108
18122
|
content: [
|
|
17109
18123
|
{
|
|
@@ -17127,17 +18141,17 @@ var init_tools = __esm({
|
|
|
17127
18141
|
}
|
|
17128
18142
|
let resolved;
|
|
17129
18143
|
if (typeof buildData === "string") {
|
|
17130
|
-
const filePath =
|
|
17131
|
-
if (!
|
|
18144
|
+
const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
|
|
18145
|
+
if (!fs7.existsSync(filePath)) {
|
|
17132
18146
|
throw new Error(`Build not found in library: ${buildData}`);
|
|
17133
18147
|
}
|
|
17134
|
-
resolved = JSON.parse(
|
|
18148
|
+
resolved = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
17135
18149
|
} else if (buildData.id && !buildData.parts) {
|
|
17136
|
-
const filePath =
|
|
17137
|
-
if (!
|
|
18150
|
+
const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
|
|
18151
|
+
if (!fs7.existsSync(filePath)) {
|
|
17138
18152
|
throw new Error(`Build not found in library: ${buildData.id}`);
|
|
17139
18153
|
}
|
|
17140
|
-
resolved = JSON.parse(
|
|
18154
|
+
resolved = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
17141
18155
|
} else {
|
|
17142
18156
|
resolved = buildData;
|
|
17143
18157
|
}
|
|
@@ -17160,13 +18174,13 @@ var init_tools = __esm({
|
|
|
17160
18174
|
const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
|
|
17161
18175
|
const builds = [];
|
|
17162
18176
|
for (const s of styles) {
|
|
17163
|
-
const dirPath =
|
|
17164
|
-
if (!
|
|
18177
|
+
const dirPath = path8.join(libraryPath, s);
|
|
18178
|
+
if (!fs7.existsSync(dirPath))
|
|
17165
18179
|
continue;
|
|
17166
|
-
const files =
|
|
18180
|
+
const files = fs7.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
|
|
17167
18181
|
for (const file of files) {
|
|
17168
18182
|
try {
|
|
17169
|
-
const content =
|
|
18183
|
+
const content = fs7.readFileSync(path8.join(dirPath, file), "utf-8");
|
|
17170
18184
|
const data = JSON.parse(content);
|
|
17171
18185
|
builds.push({
|
|
17172
18186
|
id: data.id || `${s}/${file.replace(".json", "")}`,
|
|
@@ -17205,11 +18219,11 @@ var init_tools = __esm({
|
|
|
17205
18219
|
if (!id) {
|
|
17206
18220
|
throw new Error("Build ID is required for get_build");
|
|
17207
18221
|
}
|
|
17208
|
-
const filePath =
|
|
17209
|
-
if (!
|
|
18222
|
+
const filePath = path8.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
18223
|
+
if (!fs7.existsSync(filePath)) {
|
|
17210
18224
|
throw new Error(`Build not found in library: ${id}`);
|
|
17211
18225
|
}
|
|
17212
|
-
const data = JSON.parse(
|
|
18226
|
+
const data = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
17213
18227
|
const result = {
|
|
17214
18228
|
id: data.id,
|
|
17215
18229
|
style: data.style,
|
|
@@ -17292,11 +18306,11 @@ var init_tools = __esm({
|
|
|
17292
18306
|
if (!buildId) {
|
|
17293
18307
|
throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
|
|
17294
18308
|
}
|
|
17295
|
-
const filePath =
|
|
17296
|
-
if (!
|
|
18309
|
+
const filePath = path8.join(libraryPath, `${buildId}.json`);
|
|
18310
|
+
if (!fs7.existsSync(filePath)) {
|
|
17297
18311
|
throw new Error(`Build not found in library: ${buildId}`);
|
|
17298
18312
|
}
|
|
17299
|
-
const content =
|
|
18313
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
17300
18314
|
const buildData = JSON.parse(content);
|
|
17301
18315
|
const buildName = buildId.split("/").pop() || buildId;
|
|
17302
18316
|
expandedBuilds.push({
|
|
@@ -17611,10 +18625,10 @@ var init_tools = __esm({
|
|
|
17611
18625
|
this.safety.recordOperation({ kind: "audio", summary: `sound ${options.soundId} under ${options.parentPath}` });
|
|
17612
18626
|
return result;
|
|
17613
18627
|
}
|
|
17614
|
-
async audioPlaySound(
|
|
17615
|
-
if (!
|
|
18628
|
+
async audioPlaySound(path9, instance_id) {
|
|
18629
|
+
if (!path9)
|
|
17616
18630
|
throw new Error("path is required for audio_play_sound");
|
|
17617
|
-
return this._runGeneratedLuau(buildPlaySoundLuau({ path:
|
|
18631
|
+
return this._runGeneratedLuau(buildPlaySoundLuau({ path: path9 }), instance_id);
|
|
17618
18632
|
}
|
|
17619
18633
|
async animationCreate(options, instance_id) {
|
|
17620
18634
|
if (!options?.parentPath || options?.animationId === void 0)
|
|
@@ -17721,21 +18735,21 @@ var init_tools = __esm({
|
|
|
17721
18735
|
if (!res.ok)
|
|
17722
18736
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Download failed: HTTP ${res.status} for ${options.source}` }) }] };
|
|
17723
18737
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
17724
|
-
const ext =
|
|
17725
|
-
filePath =
|
|
17726
|
-
|
|
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);
|
|
17727
18741
|
bytes = buf.length;
|
|
17728
18742
|
cleanup = true;
|
|
17729
18743
|
} else {
|
|
17730
|
-
if (!
|
|
18744
|
+
if (!fs7.existsSync(options.source))
|
|
17731
18745
|
throw new Error(`File not found: ${options.source}`);
|
|
17732
18746
|
filePath = options.source;
|
|
17733
|
-
bytes =
|
|
18747
|
+
bytes = fs7.statSync(filePath).size;
|
|
17734
18748
|
}
|
|
17735
|
-
const sha256 = crypto.createHash("sha256").update(
|
|
18749
|
+
const sha256 = crypto.createHash("sha256").update(fs7.readFileSync(filePath)).digest("hex");
|
|
17736
18750
|
let uploadRaw = {};
|
|
17737
18751
|
try {
|
|
17738
|
-
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));
|
|
17739
18753
|
const text = up.content?.find((c) => "text" in c)?.text ?? "{}";
|
|
17740
18754
|
uploadRaw = JSON.parse(text);
|
|
17741
18755
|
} catch (error) {
|
|
@@ -17743,7 +18757,7 @@ var init_tools = __esm({
|
|
|
17743
18757
|
} finally {
|
|
17744
18758
|
if (cleanup) {
|
|
17745
18759
|
try {
|
|
17746
|
-
|
|
18760
|
+
fs7.unlinkSync(filePath);
|
|
17747
18761
|
} catch {
|
|
17748
18762
|
}
|
|
17749
18763
|
}
|
|
@@ -17802,10 +18816,10 @@ var init_tools = __esm({
|
|
|
17802
18816
|
const { buffer, contentType } = await this.imageClient.generate(prompt, options ?? {});
|
|
17803
18817
|
const ext = contentType.includes("png") ? "png" : "jpg";
|
|
17804
18818
|
const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "image";
|
|
17805
|
-
const dir =
|
|
17806
|
-
|
|
17807
|
-
const file =
|
|
17808
|
-
|
|
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);
|
|
17809
18823
|
return { file, bytes: buffer.length, model: options?.model ?? DEFAULT_IMAGE_MODEL };
|
|
17810
18824
|
}
|
|
17811
18825
|
async imageGenerate(prompt, options) {
|
|
@@ -17871,11 +18885,11 @@ var init_tools = __esm({
|
|
|
17871
18885
|
return null;
|
|
17872
18886
|
}
|
|
17873
18887
|
async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
|
|
17874
|
-
if (!
|
|
18888
|
+
if (!fs7.existsSync(filePath)) {
|
|
17875
18889
|
throw new Error(`File not found: ${filePath}`);
|
|
17876
18890
|
}
|
|
17877
|
-
const fileContent =
|
|
17878
|
-
const fileName =
|
|
18891
|
+
const fileContent = fs7.readFileSync(filePath);
|
|
18892
|
+
const fileName = path8.basename(filePath);
|
|
17879
18893
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
17880
18894
|
const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
|
|
17881
18895
|
return {
|
|
@@ -17954,8 +18968,8 @@ var init_tools = __esm({
|
|
|
17954
18968
|
async getSceneSummary(instancePath, topN, instance_id) {
|
|
17955
18969
|
return this.sceneReadTools.getSceneSummary(instancePath, topN, instance_id);
|
|
17956
18970
|
}
|
|
17957
|
-
async applyMutationPlan(operations, dryRun, confirm, instance_id) {
|
|
17958
|
-
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);
|
|
17959
18973
|
}
|
|
17960
18974
|
// Recipes: proven idempotent build macros. list is pure; apply runs the recipe's
|
|
17961
18975
|
// Luau (creates/replaces named instances).
|
|
@@ -18002,7 +19016,7 @@ var init_tools = __esm({
|
|
|
18002
19016
|
// state is this and how did it get here". Also readable at roblox://repro/bundle.
|
|
18003
19017
|
// Track G2. ponytail: composes existing readers; no new persistence.
|
|
18004
19018
|
async getReproductionBundle(instance_id) {
|
|
18005
|
-
const
|
|
19019
|
+
const parse2 = (r) => {
|
|
18006
19020
|
try {
|
|
18007
19021
|
const t = r?.content?.[0]?.text;
|
|
18008
19022
|
return typeof t === "string" ? JSON.parse(t) : {};
|
|
@@ -18011,10 +19025,10 @@ var init_tools = __esm({
|
|
|
18011
19025
|
}
|
|
18012
19026
|
};
|
|
18013
19027
|
const [snapshot, recentOperations, instances, episodes] = await Promise.all([
|
|
18014
|
-
this.getWorldSnapshot(void 0, "overview", void 0, instance_id).then(
|
|
18015
|
-
Promise.resolve(this.getOperationHistory(25)).then(
|
|
18016
|
-
Promise.resolve(this.getConnectedInstances()).then(
|
|
18017
|
-
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(() => ({}))
|
|
18018
19032
|
]);
|
|
18019
19033
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
18020
19034
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -18034,17 +19048,17 @@ var init_tools = __esm({
|
|
|
18034
19048
|
async toolCatalogSearch(body) {
|
|
18035
19049
|
return this.discoveryTools.toolCatalogSearch(body);
|
|
18036
19050
|
}
|
|
18037
|
-
async getWorldSnapshot(
|
|
18038
|
-
return this.worldTools.getWorldSnapshot(
|
|
19051
|
+
async getWorldSnapshot(path9, level, topNPerClass, instance_id) {
|
|
19052
|
+
return this.worldTools.getWorldSnapshot(path9, level, topNPerClass, instance_id);
|
|
18039
19053
|
}
|
|
18040
|
-
async sceneSearch(query,
|
|
18041
|
-
return this.worldTools.sceneSearch(query,
|
|
19054
|
+
async sceneSearch(query, path9, limit, instance_id) {
|
|
19055
|
+
return this.worldTools.sceneSearch(query, path9, limit, instance_id);
|
|
18042
19056
|
}
|
|
18043
19057
|
async getNodeBatch(paths, fields, includeChildrenCount, instance_id) {
|
|
18044
19058
|
return this.worldTools.getNodeBatch(paths, fields, includeChildrenCount, instance_id);
|
|
18045
19059
|
}
|
|
18046
|
-
async getChangesSince(snapshotId,
|
|
18047
|
-
return this.worldTools.getChangesSince(snapshotId,
|
|
19060
|
+
async getChangesSince(snapshotId, path9, instance_id) {
|
|
19061
|
+
return this.worldTools.getChangesSince(snapshotId, path9, instance_id);
|
|
18048
19062
|
}
|
|
18049
19063
|
async assetPreflightInsert(assetId, instance_id) {
|
|
18050
19064
|
return this.worldTools.assetPreflightInsert(assetId, instance_id);
|
|
@@ -18083,10 +19097,10 @@ var init_tools = __esm({
|
|
|
18083
19097
|
return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
|
|
18084
19098
|
}
|
|
18085
19099
|
const bytes = Buffer.from(response.base64, "base64");
|
|
18086
|
-
const resolved =
|
|
19100
|
+
const resolved = path8.resolve(outputPath);
|
|
18087
19101
|
try {
|
|
18088
|
-
|
|
18089
|
-
|
|
19102
|
+
fs7.mkdirSync(path8.dirname(resolved), { recursive: true });
|
|
19103
|
+
fs7.writeFileSync(resolved, bytes);
|
|
18090
19104
|
} catch (err) {
|
|
18091
19105
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
|
|
18092
19106
|
}
|
|
@@ -18119,9 +19133,9 @@ var init_tools = __esm({
|
|
|
18119
19133
|
let bytes;
|
|
18120
19134
|
let sourceLabel;
|
|
18121
19135
|
if (source.path !== void 0) {
|
|
18122
|
-
const resolved =
|
|
19136
|
+
const resolved = path8.resolve(source.path);
|
|
18123
19137
|
try {
|
|
18124
|
-
bytes =
|
|
19138
|
+
bytes = fs7.readFileSync(resolved);
|
|
18125
19139
|
} catch (err) {
|
|
18126
19140
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err.message}` }) }] };
|
|
18127
19141
|
}
|
|
@@ -18314,7 +19328,7 @@ var init_tools = __esm({
|
|
|
18314
19328
|
this.instanceManager.attachInstanceId(record, connected.instanceId);
|
|
18315
19329
|
return wrapToolJsonText({ ...this.managedStatus(record), instance_id: connected.instanceId, connected: true });
|
|
18316
19330
|
}
|
|
18317
|
-
await new Promise((
|
|
19331
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
18318
19332
|
}
|
|
18319
19333
|
}
|
|
18320
19334
|
return wrapToolJsonText({ ...this.managedStatus(record), connected: false });
|
|
@@ -18346,17 +19360,17 @@ var init_tools = __esm({
|
|
|
18346
19360
|
const rawSnapshotBase64 = mutable.raw_snapshot_base64;
|
|
18347
19361
|
if (typeof rawSnapshotBase64 === "string") {
|
|
18348
19362
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
18349
|
-
const resolvedOutputPath =
|
|
18350
|
-
|
|
18351
|
-
|
|
19363
|
+
const resolvedOutputPath = path8.resolve(outputPath);
|
|
19364
|
+
fs7.mkdirSync(path8.dirname(resolvedOutputPath), { recursive: true });
|
|
19365
|
+
fs7.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
|
|
18352
19366
|
mutable.output_path = resolvedOutputPath;
|
|
18353
19367
|
}
|
|
18354
19368
|
delete mutable.raw_snapshot_base64;
|
|
18355
19369
|
}
|
|
18356
19370
|
if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
|
|
18357
|
-
const resolvedSummaryPath =
|
|
18358
|
-
|
|
18359
|
-
|
|
19371
|
+
const resolvedSummaryPath = path8.resolve(summaryOutputPath);
|
|
19372
|
+
fs7.mkdirSync(path8.dirname(resolvedSummaryPath), { recursive: true });
|
|
19373
|
+
fs7.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
|
|
18360
19374
|
mutable.summary_output_path = resolvedSummaryPath;
|
|
18361
19375
|
}
|
|
18362
19376
|
const baselineCapture = loadMicroProfilerBaseline(request.baseline, request.baseline_path);
|
|
@@ -18389,12 +19403,11 @@ var init_proxy_bridge_service = __esm({
|
|
|
18389
19403
|
ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
18390
19404
|
primaryBaseUrl;
|
|
18391
19405
|
proxyInstanceId;
|
|
18392
|
-
proxyRequestTimeout = 3e4;
|
|
18393
19406
|
cachedInstances = [];
|
|
18394
19407
|
refreshTimer;
|
|
18395
19408
|
static REFRESH_INTERVAL_MS = 1e3;
|
|
18396
19409
|
constructor(primaryBaseUrl) {
|
|
18397
|
-
super();
|
|
19410
|
+
super("");
|
|
18398
19411
|
this.primaryBaseUrl = primaryBaseUrl;
|
|
18399
19412
|
this.proxyInstanceId = randomUUID3();
|
|
18400
19413
|
this.refreshInstances();
|
|
@@ -18425,7 +19438,7 @@ var init_proxy_bridge_service = __esm({
|
|
|
18425
19438
|
}
|
|
18426
19439
|
async sendRequest(endpoint, data, targetInstanceId, targetRole) {
|
|
18427
19440
|
const controller = new AbortController();
|
|
18428
|
-
const timeoutId = setTimeout(() => controller.abort(), this.
|
|
19441
|
+
const timeoutId = setTimeout(() => controller.abort(), resolveRequestTimeout(endpoint, this.requestTimeout));
|
|
18429
19442
|
try {
|
|
18430
19443
|
const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
|
|
18431
19444
|
method: "POST",
|
|
@@ -18441,8 +19454,11 @@ var init_proxy_bridge_service = __esm({
|
|
|
18441
19454
|
});
|
|
18442
19455
|
clearTimeout(timeoutId);
|
|
18443
19456
|
if (!response.ok) {
|
|
18444
|
-
const body = await response.
|
|
18445
|
-
|
|
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"}`);
|
|
18446
19462
|
}
|
|
18447
19463
|
const result = await response.json();
|
|
18448
19464
|
if (result.error) {
|
|
@@ -18457,6 +19473,26 @@ var init_proxy_bridge_service = __esm({
|
|
|
18457
19473
|
throw err;
|
|
18458
19474
|
}
|
|
18459
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
|
+
}
|
|
18460
19496
|
cleanupOldRequests() {
|
|
18461
19497
|
}
|
|
18462
19498
|
clearAllPendingRequests() {
|
|
@@ -18688,6 +19724,12 @@ var init_setup_registry = __esm({
|
|
|
18688
19724
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18689
19725
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18690
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
|
+
}
|
|
18691
19733
|
function shouldUseLazyToolLoading(value) {
|
|
18692
19734
|
const flag = (value ?? "").trim().toLowerCase();
|
|
18693
19735
|
return !(flag === "0" || flag === "false" || flag === "off");
|
|
@@ -18722,6 +19764,7 @@ var init_server = __esm({
|
|
|
18722
19764
|
init_structured_output();
|
|
18723
19765
|
init_server_instructions();
|
|
18724
19766
|
init_resources();
|
|
19767
|
+
init_capability_policy();
|
|
18725
19768
|
BloxForgeServer = class {
|
|
18726
19769
|
server;
|
|
18727
19770
|
tools;
|
|
@@ -18738,8 +19781,10 @@ var init_server = __esm({
|
|
|
18738
19781
|
lazyTools;
|
|
18739
19782
|
activeToolNames;
|
|
18740
19783
|
registry;
|
|
19784
|
+
stdioCapabilities;
|
|
18741
19785
|
constructor(config) {
|
|
18742
19786
|
this.config = config;
|
|
19787
|
+
this.stdioCapabilities = parseCapabilities(process.env.BLOXFORGE_STDIO_CAPABILITIES);
|
|
18743
19788
|
this.allowedToolNames = new Set(config.tools.map((t) => t.name));
|
|
18744
19789
|
this.lazyTools = shouldUseLazyToolLoading(process.env.ROBLOX_MCP_LAZY_TOOLS);
|
|
18745
19790
|
this.activeToolNames = new Set(CORE_TOOLS);
|
|
@@ -18757,7 +19802,7 @@ var init_server = __esm({
|
|
|
18757
19802
|
},
|
|
18758
19803
|
instructions: SERVER_INSTRUCTIONS
|
|
18759
19804
|
});
|
|
18760
|
-
this.bridge = new BridgeService();
|
|
19805
|
+
this.bridge = new BridgeService("");
|
|
18761
19806
|
this.tools = new RobloxStudioTools(this.bridge);
|
|
18762
19807
|
this.registry = new ToolRegistry();
|
|
18763
19808
|
registerContractedTools(this.registry, this.tools);
|
|
@@ -18804,6 +19849,11 @@ var init_server = __esm({
|
|
|
18804
19849
|
if (!this.allowedToolNames.has(name)) {
|
|
18805
19850
|
throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
|
|
18806
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
|
+
}
|
|
18807
19857
|
try {
|
|
18808
19858
|
const registryResult = await this.registry.callTool(name, this.tools, args ?? {});
|
|
18809
19859
|
if (registryResult !== null && registryResult !== void 0) {
|
|
@@ -18888,7 +19938,10 @@ var init_server = __esm({
|
|
|
18888
19938
|
}
|
|
18889
19939
|
async run() {
|
|
18890
19940
|
const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
|
|
18891
|
-
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
|
+
}
|
|
18892
19945
|
let bridgeMode = "primary";
|
|
18893
19946
|
let httpHandle;
|
|
18894
19947
|
let primaryApp;
|
|
@@ -18898,6 +19951,7 @@ var init_server = __esm({
|
|
|
18898
19951
|
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, this.registry);
|
|
18899
19952
|
const result = await listenWithRetry(primaryApp, host, basePort, 1);
|
|
18900
19953
|
httpHandle = result.server;
|
|
19954
|
+
this.bridge.enableJournal();
|
|
18901
19955
|
boundPort = result.port;
|
|
18902
19956
|
console.error(`HTTP server listening on ${host}:${boundPort} for Studio plugin (primary mode)`);
|
|
18903
19957
|
console.error(`Streamable HTTP MCP endpoint: http://localhost:${boundPort}/mcp`);
|
|
@@ -18910,12 +19964,13 @@ var init_server = __esm({
|
|
|
18910
19964
|
console.error(`Port ${basePort} in use - entering proxy mode (forwarding to localhost:${basePort})`);
|
|
18911
19965
|
const promotionIntervalMs = parseInt(process.env.ROBLOX_STUDIO_PROXY_PROMOTION_INTERVAL_MS || "5000");
|
|
18912
19966
|
promotionInterval = setInterval(async () => {
|
|
18913
|
-
const candidateBridge = new BridgeService();
|
|
19967
|
+
const candidateBridge = new BridgeService("");
|
|
18914
19968
|
const candidateTools = new RobloxStudioTools(candidateBridge);
|
|
18915
19969
|
const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config);
|
|
18916
19970
|
try {
|
|
18917
19971
|
const result = await listenWithRetry(candidateApp, host, basePort, 1);
|
|
18918
19972
|
const oldBridge = this.bridge;
|
|
19973
|
+
candidateBridge.enableJournal();
|
|
18919
19974
|
this.bridge = candidateBridge;
|
|
18920
19975
|
this.tools = candidateTools;
|
|
18921
19976
|
if (oldBridge instanceof ProxyBridgeService) {
|
|
@@ -18977,7 +20032,18 @@ var init_server = __esm({
|
|
|
18977
20032
|
this.bridge.cleanupOldRequests();
|
|
18978
20033
|
this.bridge.cleanupStaleInstances();
|
|
18979
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
|
+
});
|
|
18980
20043
|
const shutdown = async () => {
|
|
20044
|
+
if (shuttingDown)
|
|
20045
|
+
return;
|
|
20046
|
+
shuttingDown = true;
|
|
18981
20047
|
console.error("Shutting down MCP server...");
|
|
18982
20048
|
clearInterval(activityInterval);
|
|
18983
20049
|
clearInterval(cleanupInterval);
|
|
@@ -18986,12 +20052,14 @@ var init_server = __esm({
|
|
|
18986
20052
|
if (this.bridge instanceof ProxyBridgeService) {
|
|
18987
20053
|
this.bridge.stop();
|
|
18988
20054
|
}
|
|
20055
|
+
if (primaryApp)
|
|
20056
|
+
primaryApp.setMCPServerActive(false);
|
|
20057
|
+
if (legacyApp)
|
|
20058
|
+
legacyApp.setMCPServerActive(false);
|
|
20059
|
+
this.bridge.clearAllPendingRequests();
|
|
18989
20060
|
await this.server.close().catch(() => {
|
|
18990
20061
|
});
|
|
18991
|
-
|
|
18992
|
-
httpHandle.close();
|
|
18993
|
-
if (legacyHandle)
|
|
18994
|
-
legacyHandle.close();
|
|
20062
|
+
await Promise.all([closeHttp(httpHandle), closeHttp(legacyHandle)]);
|
|
18995
20063
|
process.exit(0);
|
|
18996
20064
|
};
|
|
18997
20065
|
process.on("SIGTERM", shutdown);
|
|
@@ -19004,16 +20072,23 @@ var init_server = __esm({
|
|
|
19004
20072
|
}
|
|
19005
20073
|
});
|
|
19006
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
|
+
|
|
19007
20082
|
// ../core/dist/install-plugin-helpers.js
|
|
19008
|
-
import { existsSync as
|
|
20083
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
19009
20084
|
import { execSync } from "child_process";
|
|
19010
|
-
import { join as
|
|
19011
|
-
import { homedir as
|
|
20085
|
+
import { join as join8 } from "path";
|
|
20086
|
+
import { homedir as homedir6 } from "os";
|
|
19012
20087
|
function isWSL() {
|
|
19013
20088
|
if (process.platform !== "linux")
|
|
19014
20089
|
return false;
|
|
19015
20090
|
try {
|
|
19016
|
-
const v =
|
|
20091
|
+
const v = readFileSync8("/proc/version", "utf8");
|
|
19017
20092
|
return /microsoft|wsl/i.test(v);
|
|
19018
20093
|
} catch {
|
|
19019
20094
|
return false;
|
|
@@ -19031,7 +20106,7 @@ function getWindowsUserPluginsDir() {
|
|
|
19031
20106
|
}).toString().trim();
|
|
19032
20107
|
if (!linuxPath)
|
|
19033
20108
|
return null;
|
|
19034
|
-
return
|
|
20109
|
+
return join8(linuxPath, "Roblox", "Plugins");
|
|
19035
20110
|
} catch {
|
|
19036
20111
|
return null;
|
|
19037
20112
|
}
|
|
@@ -19040,7 +20115,7 @@ function getPluginsFolder() {
|
|
|
19040
20115
|
if (process.env.MCP_PLUGINS_DIR)
|
|
19041
20116
|
return process.env.MCP_PLUGINS_DIR;
|
|
19042
20117
|
if (process.platform === "win32") {
|
|
19043
|
-
return
|
|
20118
|
+
return join8(process.env.LOCALAPPDATA || join8(homedir6(), "AppData", "Local"), "Roblox", "Plugins");
|
|
19044
20119
|
}
|
|
19045
20120
|
if (isWSL()) {
|
|
19046
20121
|
const win = getWindowsUserPluginsDir();
|
|
@@ -19048,11 +20123,11 @@ function getPluginsFolder() {
|
|
|
19048
20123
|
return win;
|
|
19049
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.");
|
|
19050
20125
|
}
|
|
19051
|
-
return
|
|
20126
|
+
return join8(homedir6(), "Documents", "Roblox", "Plugins");
|
|
19052
20127
|
}
|
|
19053
20128
|
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
19054
|
-
const otherDest =
|
|
19055
|
-
if (!
|
|
20129
|
+
const otherDest = join8(pluginsFolder, otherAssetName);
|
|
20130
|
+
if (!existsSync7(otherDest))
|
|
19056
20131
|
return;
|
|
19057
20132
|
if (replace) {
|
|
19058
20133
|
try {
|
|
@@ -19092,6 +20167,8 @@ var init_dist = __esm({
|
|
|
19092
20167
|
init_server();
|
|
19093
20168
|
init_http_server();
|
|
19094
20169
|
init_bridge_service();
|
|
20170
|
+
init_protocol_manifest();
|
|
20171
|
+
init_quality_tools();
|
|
19095
20172
|
init_tools();
|
|
19096
20173
|
init_studio_client();
|
|
19097
20174
|
init_definitions();
|
|
@@ -19109,13 +20186,13 @@ __export(install_plugin_exports, {
|
|
|
19109
20186
|
installBundledPlugin: () => installBundledPlugin,
|
|
19110
20187
|
installPlugin: () => installPlugin
|
|
19111
20188
|
});
|
|
19112
|
-
import { copyFileSync as
|
|
19113
|
-
import { dirname as
|
|
20189
|
+
import { copyFileSync as copyFileSync3, createWriteStream, existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync9, unlinkSync as unlinkSync3 } from "fs";
|
|
20190
|
+
import { dirname as dirname8, join as join9 } from "path";
|
|
19114
20191
|
import { fileURLToPath } from "url";
|
|
19115
20192
|
import { get } from "https";
|
|
19116
20193
|
function httpsGet(url) {
|
|
19117
|
-
return new Promise((
|
|
19118
|
-
const req = get(url, { headers: { "User-Agent": "bloxforge-inspector" } },
|
|
20194
|
+
return new Promise((resolve7, reject) => {
|
|
20195
|
+
const req = get(url, { headers: { "User-Agent": "bloxforge-inspector" } }, resolve7);
|
|
19119
20196
|
req.on("error", reject);
|
|
19120
20197
|
req.setTimeout(TIMEOUT_MS, () => {
|
|
19121
20198
|
req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
|
|
@@ -19133,7 +20210,7 @@ async function download(url, dest, redirects = 0) {
|
|
|
19133
20210
|
if (res.statusCode !== 200) {
|
|
19134
20211
|
throw new Error(`Download failed: HTTP ${res.statusCode}`);
|
|
19135
20212
|
}
|
|
19136
|
-
return new Promise((
|
|
20213
|
+
return new Promise((resolve7, reject) => {
|
|
19137
20214
|
const file = createWriteStream(dest);
|
|
19138
20215
|
const cleanup = (err) => {
|
|
19139
20216
|
file.close(() => {
|
|
@@ -19147,7 +20224,7 @@ async function download(url, dest, redirects = 0) {
|
|
|
19147
20224
|
res.pipe(file);
|
|
19148
20225
|
file.on("finish", () => {
|
|
19149
20226
|
file.close();
|
|
19150
|
-
|
|
20227
|
+
resolve7();
|
|
19151
20228
|
});
|
|
19152
20229
|
file.on("error", cleanup);
|
|
19153
20230
|
res.on("error", cleanup);
|
|
@@ -19170,8 +20247,8 @@ function prepareInstall({
|
|
|
19170
20247
|
warn
|
|
19171
20248
|
}) {
|
|
19172
20249
|
const pluginsFolder = getPluginsFolder();
|
|
19173
|
-
if (!
|
|
19174
|
-
|
|
20250
|
+
if (!existsSync8(pluginsFolder)) {
|
|
20251
|
+
mkdirSync8(pluginsFolder, { recursive: true });
|
|
19175
20252
|
}
|
|
19176
20253
|
handleVariantConflict({
|
|
19177
20254
|
pluginsFolder,
|
|
@@ -19183,23 +20260,23 @@ function prepareInstall({
|
|
|
19183
20260
|
return pluginsFolder;
|
|
19184
20261
|
}
|
|
19185
20262
|
function bundledAssetPath() {
|
|
19186
|
-
const currentDir =
|
|
20263
|
+
const currentDir = dirname8(fileURLToPath(import.meta.url));
|
|
19187
20264
|
const candidates = [
|
|
19188
|
-
|
|
19189
|
-
|
|
20265
|
+
join9(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
20266
|
+
join9(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
19190
20267
|
];
|
|
19191
|
-
return candidates.find((candidate) =>
|
|
20268
|
+
return candidates.find((candidate) => existsSync8(candidate)) ?? null;
|
|
19192
20269
|
}
|
|
19193
20270
|
function packageVersion() {
|
|
19194
|
-
const currentDir =
|
|
19195
|
-
const pkg = JSON.parse(
|
|
20271
|
+
const currentDir = dirname8(fileURLToPath(import.meta.url));
|
|
20272
|
+
const pkg = JSON.parse(readFileSync9(join9(currentDir, "..", "package.json"), "utf8"));
|
|
19196
20273
|
if (!pkg.version) {
|
|
19197
20274
|
throw new Error("Package version not found");
|
|
19198
20275
|
}
|
|
19199
20276
|
return pkg.version;
|
|
19200
20277
|
}
|
|
19201
20278
|
function bundledPluginVersion(source) {
|
|
19202
|
-
const match =
|
|
20279
|
+
const match = readFileSync9(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
19203
20280
|
return match ? match[1] : null;
|
|
19204
20281
|
}
|
|
19205
20282
|
function assertBundledPluginVersion(source) {
|
|
@@ -19212,9 +20289,9 @@ function assertBundledPluginVersion(source) {
|
|
|
19212
20289
|
}
|
|
19213
20290
|
}
|
|
19214
20291
|
function filesMatch(a, b) {
|
|
19215
|
-
if (!
|
|
19216
|
-
const aBytes =
|
|
19217
|
-
const bBytes =
|
|
20292
|
+
if (!existsSync8(b)) return false;
|
|
20293
|
+
const aBytes = readFileSync9(a);
|
|
20294
|
+
const bBytes = readFileSync9(b);
|
|
19218
20295
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
19219
20296
|
}
|
|
19220
20297
|
async function installBundledPlugin(options = {}) {
|
|
@@ -19227,9 +20304,9 @@ async function installBundledPlugin(options = {}) {
|
|
|
19227
20304
|
}
|
|
19228
20305
|
assertBundledPluginVersion(source);
|
|
19229
20306
|
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
19230
|
-
const dest =
|
|
20307
|
+
const dest = join9(pluginsFolder, ASSET_NAME);
|
|
19231
20308
|
if (filesMatch(source, dest)) return;
|
|
19232
|
-
|
|
20309
|
+
copyFileSync3(source, dest);
|
|
19233
20310
|
log(`Installed ${ASSET_NAME} to ${dest}`);
|
|
19234
20311
|
}
|
|
19235
20312
|
async function installPlugin(options = {}) {
|
|
@@ -19240,12 +20317,12 @@ async function installPlugin(options = {}) {
|
|
|
19240
20317
|
const bundled = bundledAssetPath();
|
|
19241
20318
|
if (bundled) {
|
|
19242
20319
|
assertBundledPluginVersion(bundled);
|
|
19243
|
-
const dest2 =
|
|
20320
|
+
const dest2 = join9(pluginsFolder, ASSET_NAME);
|
|
19244
20321
|
if (filesMatch(bundled, dest2)) {
|
|
19245
20322
|
log(`${ASSET_NAME} already installed.`);
|
|
19246
20323
|
return;
|
|
19247
20324
|
}
|
|
19248
|
-
|
|
20325
|
+
copyFileSync3(bundled, dest2);
|
|
19249
20326
|
log(`Installed bundled ${ASSET_NAME} to ${dest2}`);
|
|
19250
20327
|
return;
|
|
19251
20328
|
}
|
|
@@ -19255,7 +20332,7 @@ async function installPlugin(options = {}) {
|
|
|
19255
20332
|
if (!asset) {
|
|
19256
20333
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
19257
20334
|
}
|
|
19258
|
-
const dest =
|
|
20335
|
+
const dest = join9(pluginsFolder, ASSET_NAME);
|
|
19259
20336
|
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
19260
20337
|
await download(asset.browser_download_url, dest);
|
|
19261
20338
|
log(`Installed to ${dest}`);
|