conduit-mcp 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-JDWBW44K.js → chunk-U4GZFDM3.js} +273 -56
- package/dist/chunk-U4GZFDM3.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/{cloud-HQIBAMRL.js → cloud-4AYJVND6.js} +2 -2
- package/dist/cloud-4AYJVND6.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/{rojo-TXV4K4PB.js → rojo-2CMKPTPS.js} +11 -2
- package/dist/rojo-2CMKPTPS.js.map +1 -0
- package/package.json +2 -2
- package/plugin/Conduit.rbxm +0 -0
- package/dist/chunk-JDWBW44K.js.map +0 -1
- package/dist/cloud-HQIBAMRL.js.map +0 -1
- package/dist/rojo-TXV4K4PB.js.map +0 -1
|
@@ -10,7 +10,7 @@ import { WebSocketServer, WebSocket } from "ws";
|
|
|
10
10
|
// src/protocol.ts
|
|
11
11
|
import { randomUUID } from "crypto";
|
|
12
12
|
function generateId() {
|
|
13
|
-
return randomUUID().slice(0,
|
|
13
|
+
return randomUUID().slice(0, 16);
|
|
14
14
|
}
|
|
15
15
|
function isHeartbeat(msg) {
|
|
16
16
|
return typeof msg === "object" && msg !== null && "type" in msg && msg.type === "heartbeat";
|
|
@@ -40,9 +40,10 @@ var log = {
|
|
|
40
40
|
// src/bridge.ts
|
|
41
41
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
42
42
|
var HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
|
|
43
|
-
var HEARTBEAT_TIMEOUT_MS =
|
|
43
|
+
var HEARTBEAT_TIMEOUT_MS = 35e3;
|
|
44
44
|
var LONG_POLL_TIMEOUT_MS = 25e3;
|
|
45
45
|
var MAX_PORT_RETRIES = 10;
|
|
46
|
+
var REGISTRATION_TIMEOUT_MS = 1e4;
|
|
46
47
|
var Bridge = class extends EventEmitter {
|
|
47
48
|
constructor(port = 3200) {
|
|
48
49
|
super();
|
|
@@ -65,8 +66,10 @@ var Bridge = class extends EventEmitter {
|
|
|
65
66
|
httpPendingCommands = /* @__PURE__ */ new Map();
|
|
66
67
|
httpPollWaiters = /* @__PURE__ */ new Map();
|
|
67
68
|
get isConnected() {
|
|
68
|
-
const studio
|
|
69
|
-
|
|
69
|
+
for (const [, studio] of this.studios) {
|
|
70
|
+
if (studio.ws.readyState === WebSocket.OPEN) return true;
|
|
71
|
+
}
|
|
72
|
+
return this.httpStudios.size > 0;
|
|
70
73
|
}
|
|
71
74
|
get listeningPort() {
|
|
72
75
|
return this.actualPort;
|
|
@@ -92,7 +95,35 @@ var Bridge = class extends EventEmitter {
|
|
|
92
95
|
log.info(`Active studio set to: ${studioId}`);
|
|
93
96
|
}
|
|
94
97
|
// ── Server lifecycle ───────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Try to shut down a stale Conduit instance on the target port.
|
|
100
|
+
* Returns true if the port was freed (or was already free).
|
|
101
|
+
*/
|
|
102
|
+
async evictStaleInstance(port) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const req = http.request(
|
|
105
|
+
{
|
|
106
|
+
hostname: "127.0.0.1",
|
|
107
|
+
port,
|
|
108
|
+
path: "/shutdown",
|
|
109
|
+
method: "POST",
|
|
110
|
+
timeout: 2e3
|
|
111
|
+
},
|
|
112
|
+
(res) => {
|
|
113
|
+
res.resume();
|
|
114
|
+
setTimeout(() => resolve(true), 500);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
req.on("error", () => resolve(false));
|
|
118
|
+
req.on("timeout", () => {
|
|
119
|
+
req.destroy();
|
|
120
|
+
resolve(false);
|
|
121
|
+
});
|
|
122
|
+
req.end();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
95
125
|
async start() {
|
|
126
|
+
await this.evictStaleInstance(this.port);
|
|
96
127
|
return new Promise((resolve, reject) => {
|
|
97
128
|
let attempts = 0;
|
|
98
129
|
const tryPort = (port) => {
|
|
@@ -102,6 +133,9 @@ var Bridge = class extends EventEmitter {
|
|
|
102
133
|
server.once("error", (err) => {
|
|
103
134
|
if (err.code === "EADDRINUSE" && attempts < MAX_PORT_RETRIES) {
|
|
104
135
|
attempts++;
|
|
136
|
+
log.warn(
|
|
137
|
+
`Port ${port} still in use after eviction \u2014 trying ${port + 1}`
|
|
138
|
+
);
|
|
105
139
|
tryPort(port + 1);
|
|
106
140
|
} else {
|
|
107
141
|
reject(err);
|
|
@@ -209,30 +243,33 @@ var Bridge = class extends EventEmitter {
|
|
|
209
243
|
if (active && active.ws.readyState === WebSocket.OPEN) {
|
|
210
244
|
return active;
|
|
211
245
|
}
|
|
212
|
-
|
|
213
|
-
const [studioId, studio] = this.studios.entries().next().value;
|
|
246
|
+
for (const [studioId, studio] of this.studios) {
|
|
214
247
|
if (studio.ws.readyState === WebSocket.OPEN) {
|
|
215
248
|
this.activeStudioId = studioId;
|
|
249
|
+
log.info(`Auto-selected active studio: ${studioId}`);
|
|
216
250
|
return studio;
|
|
217
251
|
}
|
|
218
252
|
}
|
|
219
|
-
|
|
220
|
-
return void 0;
|
|
221
|
-
}
|
|
222
|
-
throw new Error(
|
|
223
|
-
`Multiple Studio instances connected but none is active. Use set_active_studio to choose one. Connected: ${Array.from(
|
|
224
|
-
this.studios.values()
|
|
225
|
-
).map((s) => `${s.info.studioId} (${s.info.placeName ?? "unknown"})`).join(", ")}`
|
|
226
|
-
);
|
|
253
|
+
return void 0;
|
|
227
254
|
}
|
|
228
255
|
// ── WebSocket handling ─────────────────────────────────────────
|
|
229
256
|
handleWsConnection(ws) {
|
|
230
257
|
log.info("New WebSocket connection \u2014 waiting for registration");
|
|
231
258
|
this.pendingWs.add(ws);
|
|
259
|
+
const registrationTimer = setTimeout(() => {
|
|
260
|
+
if (this.pendingWs.has(ws)) {
|
|
261
|
+
log.warn(
|
|
262
|
+
"WebSocket failed to register within timeout \u2014 closing"
|
|
263
|
+
);
|
|
264
|
+
this.pendingWs.delete(ws);
|
|
265
|
+
ws.close(1008, "Registration timeout");
|
|
266
|
+
}
|
|
267
|
+
}, REGISTRATION_TIMEOUT_MS);
|
|
232
268
|
const onFirstMessage = (data) => {
|
|
233
269
|
try {
|
|
234
270
|
const msg = JSON.parse(data.toString());
|
|
235
271
|
if (isStudioRegistration(msg)) {
|
|
272
|
+
clearTimeout(registrationTimer);
|
|
236
273
|
ws.removeListener("message", onFirstMessage);
|
|
237
274
|
this.pendingWs.delete(ws);
|
|
238
275
|
this.registerStudio(ws, {
|
|
@@ -243,9 +280,10 @@ var Bridge = class extends EventEmitter {
|
|
|
243
280
|
});
|
|
244
281
|
return;
|
|
245
282
|
}
|
|
283
|
+
clearTimeout(registrationTimer);
|
|
246
284
|
ws.removeListener("message", onFirstMessage);
|
|
247
285
|
this.pendingWs.delete(ws);
|
|
248
|
-
const syntheticId =
|
|
286
|
+
const syntheticId = `studio-legacy-${Date.now()}`;
|
|
249
287
|
log.info(
|
|
250
288
|
"Legacy plugin detected (no registration), assigning ID: " + syntheticId
|
|
251
289
|
);
|
|
@@ -259,12 +297,17 @@ var Bridge = class extends EventEmitter {
|
|
|
259
297
|
}
|
|
260
298
|
};
|
|
261
299
|
ws.on("message", onFirstMessage);
|
|
262
|
-
|
|
300
|
+
const onEarlyClose = () => {
|
|
301
|
+
clearTimeout(registrationTimer);
|
|
263
302
|
this.pendingWs.delete(ws);
|
|
264
|
-
}
|
|
265
|
-
|
|
303
|
+
};
|
|
304
|
+
const onEarlyError = (err) => {
|
|
266
305
|
log.warn("WebSocket error:", err.message);
|
|
267
|
-
}
|
|
306
|
+
};
|
|
307
|
+
ws.on("close", onEarlyClose);
|
|
308
|
+
ws.on("error", onEarlyError);
|
|
309
|
+
ws.__earlyClose = onEarlyClose;
|
|
310
|
+
ws.__earlyError = onEarlyError;
|
|
268
311
|
}
|
|
269
312
|
registerStudio(ws, info) {
|
|
270
313
|
const { studioId } = info;
|
|
@@ -283,6 +326,14 @@ var Bridge = class extends EventEmitter {
|
|
|
283
326
|
`Studio registered: ${studioId}` + (info.placeName ? ` (${info.placeName})` : "")
|
|
284
327
|
);
|
|
285
328
|
this.startHeartbeatMonitor();
|
|
329
|
+
if (ws.__earlyClose) {
|
|
330
|
+
ws.removeListener("close", ws.__earlyClose);
|
|
331
|
+
delete ws.__earlyClose;
|
|
332
|
+
}
|
|
333
|
+
if (ws.__earlyError) {
|
|
334
|
+
ws.removeListener("error", ws.__earlyError);
|
|
335
|
+
delete ws.__earlyError;
|
|
336
|
+
}
|
|
286
337
|
ws.on("message", (data) => {
|
|
287
338
|
try {
|
|
288
339
|
const msg = JSON.parse(data.toString());
|
|
@@ -303,7 +354,7 @@ var Bridge = class extends EventEmitter {
|
|
|
303
354
|
this.emit("studio-disconnected", info);
|
|
304
355
|
log.info(`Studio disconnected: ${studioId}`);
|
|
305
356
|
if (this.activeStudioId === studioId) {
|
|
306
|
-
if (this.studios.size
|
|
357
|
+
if (this.studios.size > 0) {
|
|
307
358
|
this.activeStudioId = this.studios.keys().next().value;
|
|
308
359
|
log.info(
|
|
309
360
|
`Auto-switched active studio to: ${this.activeStudioId}`
|
|
@@ -319,21 +370,21 @@ var Bridge = class extends EventEmitter {
|
|
|
319
370
|
});
|
|
320
371
|
}
|
|
321
372
|
handlePluginMessage(msg) {
|
|
322
|
-
if (
|
|
373
|
+
if (isBridgeError(msg)) {
|
|
323
374
|
const pending = this.pendingRequests.get(msg.id);
|
|
324
375
|
if (pending) {
|
|
325
376
|
clearTimeout(pending.timer);
|
|
326
377
|
this.pendingRequests.delete(msg.id);
|
|
327
|
-
pending.
|
|
378
|
+
pending.reject(new Error(`${msg.error.code}: ${msg.error.message}`));
|
|
328
379
|
}
|
|
329
380
|
return;
|
|
330
381
|
}
|
|
331
|
-
if (
|
|
382
|
+
if (isBridgeResponse(msg)) {
|
|
332
383
|
const pending = this.pendingRequests.get(msg.id);
|
|
333
384
|
if (pending) {
|
|
334
385
|
clearTimeout(pending.timer);
|
|
335
386
|
this.pendingRequests.delete(msg.id);
|
|
336
|
-
pending.
|
|
387
|
+
pending.resolve(msg.result);
|
|
337
388
|
}
|
|
338
389
|
return;
|
|
339
390
|
}
|
|
@@ -350,7 +401,8 @@ var Bridge = class extends EventEmitter {
|
|
|
350
401
|
log.warn(
|
|
351
402
|
`Heartbeat timeout for studio "${studioId}" \u2014 disconnecting`
|
|
352
403
|
);
|
|
353
|
-
studio.ws.
|
|
404
|
+
studio.ws.close(1001, "Heartbeat timeout");
|
|
405
|
+
this.lastHeartbeats.delete(studioId);
|
|
354
406
|
} else if (this.httpStudios.has(studioId)) {
|
|
355
407
|
log.warn(
|
|
356
408
|
`Heartbeat timeout for HTTP studio "${studioId}" \u2014 evicting`
|
|
@@ -376,7 +428,10 @@ var Bridge = class extends EventEmitter {
|
|
|
376
428
|
}
|
|
377
429
|
// ── HTTP fallback handling ─────────────────────────────────────
|
|
378
430
|
handleHttp(req, res) {
|
|
379
|
-
|
|
431
|
+
const origin = req.headers.origin;
|
|
432
|
+
if (origin && (origin.startsWith("http://localhost") || origin.startsWith("http://127.0.0.1"))) {
|
|
433
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
434
|
+
}
|
|
380
435
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
381
436
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
382
437
|
if (req.method === "OPTIONS") {
|
|
@@ -386,6 +441,20 @@ var Bridge = class extends EventEmitter {
|
|
|
386
441
|
}
|
|
387
442
|
const parsedUrl = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
388
443
|
const pathname = parsedUrl.pathname;
|
|
444
|
+
if (req.method === "POST" && pathname === "/shutdown") {
|
|
445
|
+
if (req.headers.origin) {
|
|
446
|
+
res.writeHead(403);
|
|
447
|
+
res.end("Forbidden");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
log.info("Received shutdown request from new Conduit instance");
|
|
451
|
+
res.writeHead(200);
|
|
452
|
+
res.end("ok");
|
|
453
|
+
setImmediate(() => {
|
|
454
|
+
this.emit("shutdown");
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
389
458
|
if (req.method === "GET" && pathname === "/health") {
|
|
390
459
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
391
460
|
res.end(
|
|
@@ -393,7 +462,8 @@ var Bridge = class extends EventEmitter {
|
|
|
393
462
|
status: "ok",
|
|
394
463
|
connected: this.isConnected,
|
|
395
464
|
port: this.actualPort,
|
|
396
|
-
studios: this.getStudios()
|
|
465
|
+
studios: this.getStudios(),
|
|
466
|
+
activeStudioId: this.activeStudioId
|
|
397
467
|
})
|
|
398
468
|
);
|
|
399
469
|
return;
|
|
@@ -404,6 +474,11 @@ var Bridge = class extends EventEmitter {
|
|
|
404
474
|
return;
|
|
405
475
|
}
|
|
406
476
|
if (req.method === "POST" && pathname === "/result") {
|
|
477
|
+
if (req.headers.origin) {
|
|
478
|
+
res.writeHead(403);
|
|
479
|
+
res.end("Forbidden");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
407
482
|
this.handleResult(req, res);
|
|
408
483
|
return;
|
|
409
484
|
}
|
|
@@ -418,8 +493,10 @@ var Bridge = class extends EventEmitter {
|
|
|
418
493
|
connectedAt: Date.now()
|
|
419
494
|
};
|
|
420
495
|
this.httpStudios.set(studioId, info);
|
|
496
|
+
this.lastHeartbeats.set(studioId, Date.now());
|
|
421
497
|
this.emit("studio-connected", info);
|
|
422
498
|
log.info(`HTTP-only studio registered: ${studioId}`);
|
|
499
|
+
this.startHeartbeatMonitor();
|
|
423
500
|
}
|
|
424
501
|
this.lastHeartbeats.set(studioId, Date.now());
|
|
425
502
|
if (this.activeStudioId === null) {
|
|
@@ -464,9 +541,22 @@ var Bridge = class extends EventEmitter {
|
|
|
464
541
|
this.httpPollWaiters.set(studioId, waiters);
|
|
465
542
|
}
|
|
466
543
|
handleResult(req, res) {
|
|
467
|
-
|
|
468
|
-
|
|
544
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
545
|
+
const chunks = [];
|
|
546
|
+
let totalSize = 0;
|
|
547
|
+
req.on("data", (chunk) => {
|
|
548
|
+
totalSize += chunk.length;
|
|
549
|
+
if (totalSize > MAX_BODY_SIZE) {
|
|
550
|
+
req.destroy();
|
|
551
|
+
res.writeHead(413);
|
|
552
|
+
res.end("Request body too large");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
chunks.push(chunk);
|
|
556
|
+
});
|
|
469
557
|
req.on("end", () => {
|
|
558
|
+
if (totalSize > MAX_BODY_SIZE) return;
|
|
559
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
470
560
|
try {
|
|
471
561
|
const msg = JSON.parse(body);
|
|
472
562
|
this.handlePluginMessage(msg);
|
|
@@ -520,7 +610,6 @@ function truncateToTokenBudget(text, budget) {
|
|
|
520
610
|
}
|
|
521
611
|
|
|
522
612
|
// src/utils/formatting.ts
|
|
523
|
-
var DEFAULT_TOKEN_BUDGET = 4e3;
|
|
524
613
|
function formatTree(data, depth = 0, indent = "") {
|
|
525
614
|
let line = `${indent}- **${data.name}** \`${data.className}\``;
|
|
526
615
|
if (data.properties && Object.keys(data.properties).length > 0) {
|
|
@@ -528,7 +617,7 @@ function formatTree(data, depth = 0, indent = "") {
|
|
|
528
617
|
line += ` (${props})`;
|
|
529
618
|
}
|
|
530
619
|
let result = line + "\n";
|
|
531
|
-
if (data.children && depth
|
|
620
|
+
if (data.children && depth >= 0) {
|
|
532
621
|
for (const child of data.children) {
|
|
533
622
|
result += formatTree(child, depth - 1, indent + " ");
|
|
534
623
|
}
|
|
@@ -552,8 +641,8 @@ ${source}
|
|
|
552
641
|
\`\`\``;
|
|
553
642
|
}
|
|
554
643
|
function applyTokenBudget(text, maxTokens) {
|
|
555
|
-
|
|
556
|
-
const { text: result } = truncateToTokenBudget(text,
|
|
644
|
+
if (maxTokens === void 0) return text;
|
|
645
|
+
const { text: result } = truncateToTokenBudget(text, maxTokens);
|
|
557
646
|
return result;
|
|
558
647
|
}
|
|
559
648
|
function formatValue(v) {
|
|
@@ -883,6 +972,9 @@ function registerWrite(server, bridge) {
|
|
|
883
972
|
sources: params.sources,
|
|
884
973
|
targetParent: params.targetParent
|
|
885
974
|
});
|
|
975
|
+
if (result2.cloned.length === 0) {
|
|
976
|
+
return { content: [{ type: "text", text: "No instances were cloned." }] };
|
|
977
|
+
}
|
|
886
978
|
const text2 = result2.cloned.map((r) => `- \`${r.path}\` (${r.className})`).join("\n");
|
|
887
979
|
return { content: [{ type: "text", text: text2 }] };
|
|
888
980
|
}
|
|
@@ -892,6 +984,9 @@ function registerWrite(server, bridge) {
|
|
|
892
984
|
const result = await bridge.send("create_instances", {
|
|
893
985
|
operations: params.operations
|
|
894
986
|
});
|
|
987
|
+
if (result.created.length === 0) {
|
|
988
|
+
return { content: [{ type: "text", text: "No instances were created." }] };
|
|
989
|
+
}
|
|
895
990
|
const text = result.created.map((r) => `- \`${r.path}\` (${r.className})`).join("\n");
|
|
896
991
|
return { content: [{ type: "text", text }] };
|
|
897
992
|
}
|
|
@@ -956,6 +1051,9 @@ function registerWrite(server, bridge) {
|
|
|
956
1051
|
const result = await bridge.send("modify_instances", {
|
|
957
1052
|
operations: params.operations
|
|
958
1053
|
});
|
|
1054
|
+
if (result.modified.length === 0) {
|
|
1055
|
+
return { content: [{ type: "text", text: "No instances were modified." }] };
|
|
1056
|
+
}
|
|
959
1057
|
const text = result.modified.map((r) => `- \`${r.path}\` \u2014 modified: ${r.modified.join(", ")}`).join("\n");
|
|
960
1058
|
return { content: [{ type: "text", text }] };
|
|
961
1059
|
}
|
|
@@ -979,6 +1077,9 @@ function registerWrite(server, bridge) {
|
|
|
979
1077
|
const result = await bridge.send("delete_instances", {
|
|
980
1078
|
paths: params.paths
|
|
981
1079
|
});
|
|
1080
|
+
if (result.deleted.length === 0) {
|
|
1081
|
+
return { content: [{ type: "text", text: "No instances were deleted." }] };
|
|
1082
|
+
}
|
|
982
1083
|
const text = result.deleted.map((p) => `- \`${p}\` \u2014 deleted`).join("\n");
|
|
983
1084
|
return { content: [{ type: "text", text }] };
|
|
984
1085
|
}
|
|
@@ -1016,6 +1117,12 @@ function registerReadScript(server, bridge) {
|
|
|
1016
1117
|
}
|
|
1017
1118
|
},
|
|
1018
1119
|
async (params) => {
|
|
1120
|
+
if (params.lineRange && params.lineRange.end < params.lineRange.start) {
|
|
1121
|
+
return {
|
|
1122
|
+
content: [{ type: "text", text: "lineRange.end must be >= lineRange.start." }],
|
|
1123
|
+
isError: true
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1019
1126
|
const result = await bridge.send("read_script", {
|
|
1020
1127
|
path: params.path,
|
|
1021
1128
|
lineRange: params.lineRange
|
|
@@ -1061,6 +1168,30 @@ function registerWriteTools(server, bridge) {
|
|
|
1061
1168
|
}
|
|
1062
1169
|
},
|
|
1063
1170
|
async (params) => {
|
|
1171
|
+
if (params.mode === "range" && params.edits && params.edits.some((e) => e.endLine < e.startLine)) {
|
|
1172
|
+
return {
|
|
1173
|
+
content: [{ type: "text", text: "Range edit has endLine < startLine." }],
|
|
1174
|
+
isError: true
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
if (params.mode === "full" && params.source === void 0) {
|
|
1178
|
+
return {
|
|
1179
|
+
content: [{ type: "text", text: "full mode requires a `source` parameter." }],
|
|
1180
|
+
isError: true
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
if (params.mode === "range" && (!params.edits || params.edits.length === 0)) {
|
|
1184
|
+
return {
|
|
1185
|
+
content: [{ type: "text", text: "range mode requires a non-empty `edits` array." }],
|
|
1186
|
+
isError: true
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
if (params.mode === "find_replace" && !params.find) {
|
|
1190
|
+
return {
|
|
1191
|
+
content: [{ type: "text", text: "find_replace mode requires a `find` parameter." }],
|
|
1192
|
+
isError: true
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1064
1195
|
if (params.mode === "multi_replace") {
|
|
1065
1196
|
if (!params.scripts || params.scripts.length === 0) {
|
|
1066
1197
|
return { content: [{ type: "text", text: "multi_replace mode requires a non-empty `scripts` array." }] };
|
|
@@ -1160,9 +1291,10 @@ function register4(server, bridge) {
|
|
|
1160
1291
|
"playtest",
|
|
1161
1292
|
{
|
|
1162
1293
|
title: "Playtest Control & Virtual Input",
|
|
1163
|
-
description: "Control Roblox Studio playtesting and simulate user input.\n\nActions:\n- `start`: Begin a playtest session.\n- `stop`: End the current playtest.\n- `execute`: Run Lua code in the running game context.\n- `get_output`: Get console/log output from Studio (works in edit mode and during playtest).\n- `inspect`: Evaluate a Luau expression and return the typed result (requires active playtest).\n- `navigate`: Walk the player character to a position using PathfindingService (requires client playtest).\n- `mouse_click`: Simulate a mouse click at screen coordinates.\n- `mouse_move`: Move the virtual mouse to screen coordinates.\n- `key_press`: Press and release a key.\n- `key_down`: Hold a key down.\n- `key_up`: Release a held key.",
|
|
1294
|
+
description: "Control Roblox Studio playtesting and simulate user input.\n\nActions:\n- `start`: Begin a playtest session. Defaults to Play mode (F5, full client with player character). Set mode='run' for Run mode (F8, server-only, no player).\n- `stop`: End the current playtest.\n- `execute`: Run Lua code in the running game context.\n- `get_output`: Get console/log output from Studio (works in edit mode and during playtest).\n- `inspect`: Evaluate a Luau expression and return the typed result (requires active playtest).\n- `navigate`: Walk the player character to a position using PathfindingService (requires client playtest).\n- `mouse_click`: Simulate a mouse click at screen coordinates.\n- `mouse_move`: Move the virtual mouse to screen coordinates.\n- `key_press`: Press and release a key.\n- `key_down`: Hold a key down.\n- `key_up`: Release a held key.",
|
|
1164
1295
|
inputSchema: z4.object({
|
|
1165
1296
|
action: z4.enum(["start", "stop", "execute", "get_output", "inspect", "navigate", "mouse_click", "mouse_move", "key_press", "key_down", "key_up"]).describe("Playtest action"),
|
|
1297
|
+
mode: z4.enum(["play", "run"]).default("play").describe("Playtest mode: 'play' (F5, full client with player) or 'run' (F8, server-only). Default: play"),
|
|
1166
1298
|
code: z4.string().optional().describe("Lua code to execute (for 'execute' action)"),
|
|
1167
1299
|
// get_output params
|
|
1168
1300
|
messageTypes: z4.array(z4.string()).optional().describe("Filter by message types: 'MessageOutput', 'MessageWarning', 'MessageError', 'MessageInfo' (for 'get_output')"),
|
|
@@ -1245,6 +1377,18 @@ ${result2.message}` : ""}`;
|
|
|
1245
1377
|
return { content: [{ type: "text", text: text2 }] };
|
|
1246
1378
|
}
|
|
1247
1379
|
if (params.action === "mouse_click" || params.action === "mouse_move" || params.action === "key_press" || params.action === "key_down" || params.action === "key_up") {
|
|
1380
|
+
if ((params.action === "mouse_click" || params.action === "mouse_move") && (params.x === void 0 || params.y === void 0)) {
|
|
1381
|
+
return {
|
|
1382
|
+
content: [{ type: "text", text: `${params.action} requires \`x\` and \`y\` parameters.` }],
|
|
1383
|
+
isError: true
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
if ((params.action === "key_press" || params.action === "key_down" || params.action === "key_up") && !params.key) {
|
|
1387
|
+
return {
|
|
1388
|
+
content: [{ type: "text", text: `${params.action} requires a \`key\` parameter.` }],
|
|
1389
|
+
isError: true
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1248
1392
|
const result2 = await bridge.send("virtual_input", {
|
|
1249
1393
|
action: params.action,
|
|
1250
1394
|
x: params.x,
|
|
@@ -1258,9 +1402,16 @@ ${result2.message}` : ""}`;
|
|
|
1258
1402
|
]
|
|
1259
1403
|
};
|
|
1260
1404
|
}
|
|
1405
|
+
if (params.action === "execute" && !params.code) {
|
|
1406
|
+
return {
|
|
1407
|
+
content: [{ type: "text", text: "execute action requires a `code` parameter." }],
|
|
1408
|
+
isError: true
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1261
1411
|
const result = await bridge.send("playtest", {
|
|
1262
1412
|
action: params.action,
|
|
1263
|
-
code: params.code
|
|
1413
|
+
code: params.code,
|
|
1414
|
+
mode: params.mode
|
|
1264
1415
|
});
|
|
1265
1416
|
let text;
|
|
1266
1417
|
if (params.action === "execute") {
|
|
@@ -1281,7 +1432,10 @@ ${outputText}
|
|
|
1281
1432
|
text = lines.length > 0 ? lines.join("\n\n") : `Execution completed (${result.status})`;
|
|
1282
1433
|
text = applyTokenBudget(text, void 0);
|
|
1283
1434
|
} else {
|
|
1284
|
-
|
|
1435
|
+
const modeInfo = result.mode ? ` (${result.mode} mode)` : "";
|
|
1436
|
+
const extra = result.message ? `
|
|
1437
|
+
${result.message}` : "";
|
|
1438
|
+
text = `Playtest ${params.action}${modeInfo}: ${result.status}${extra}`;
|
|
1285
1439
|
}
|
|
1286
1440
|
return { content: [{ type: "text", text }] };
|
|
1287
1441
|
}
|
|
@@ -1343,6 +1497,14 @@ function register5(server, bridge) {
|
|
|
1343
1497
|
const text2 = `Updated ${result2.modified?.length ?? 0} setting(s): ${result2.modified?.join(", ") ?? "none"}`;
|
|
1344
1498
|
return { content: [{ type: "text", text: text2 }] };
|
|
1345
1499
|
}
|
|
1500
|
+
if (params.action === "terrain_fill") {
|
|
1501
|
+
if (!params.region) {
|
|
1502
|
+
return { content: [{ type: "text", text: "terrain_fill requires a `region` parameter." }], isError: true };
|
|
1503
|
+
}
|
|
1504
|
+
if (!params.material) {
|
|
1505
|
+
return { content: [{ type: "text", text: "terrain_fill requires a `material` parameter." }], isError: true };
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1346
1508
|
const terrainAction = params.action.replace("terrain_", "");
|
|
1347
1509
|
const result = await bridge.send("terrain", {
|
|
1348
1510
|
action: terrainAction,
|
|
@@ -1666,6 +1828,10 @@ function formatSearchResults(results) {
|
|
|
1666
1828
|
|
|
1667
1829
|
// src/tools/utility.ts
|
|
1668
1830
|
function register7(server, bridge) {
|
|
1831
|
+
const transactionState = /* @__PURE__ */ new Map();
|
|
1832
|
+
bridge.on("studio-disconnected", (info) => {
|
|
1833
|
+
transactionState.delete(info.studioId);
|
|
1834
|
+
});
|
|
1669
1835
|
server.registerTool(
|
|
1670
1836
|
"undo_redo",
|
|
1671
1837
|
{
|
|
@@ -1754,7 +1920,7 @@ function register7(server, bridge) {
|
|
|
1754
1920
|
"transaction",
|
|
1755
1921
|
{
|
|
1756
1922
|
title: "Transaction Control",
|
|
1757
|
-
description: "Group multiple mutating tool calls into a single Ctrl+Z undo point.\n\nActions:\n- `begin`: Start a transaction. All subsequent writes share one undo recording.\n- `commit`: Finish
|
|
1923
|
+
description: "Group multiple mutating tool calls into a single Ctrl+Z undo point.\n\nUsage: ALWAYS call `begin` first, then make your changes, then call `commit` or `rollback`. Calling `commit` or `rollback` without a prior `begin` will error.\n\nActions:\n- `begin`: Start a transaction. All subsequent writes share one undo recording.\n- `commit`: Finish an open transaction and commit all changes as one undo point. Requires a prior `begin`.\n- `rollback`: Cancel an open transaction and undo all changes made since begin. Requires a prior `begin`.\n\nTransactions auto-rollback after 60 seconds if not committed.",
|
|
1758
1924
|
inputSchema: z7.object({
|
|
1759
1925
|
action: z7.enum(["begin", "commit", "rollback"]).describe("Transaction action"),
|
|
1760
1926
|
name: z7.string().optional().describe("Transaction name for the undo history (for 'begin' action)")
|
|
@@ -1767,31 +1933,53 @@ function register7(server, bridge) {
|
|
|
1767
1933
|
}
|
|
1768
1934
|
},
|
|
1769
1935
|
async (params) => {
|
|
1936
|
+
const studioId = bridge.getActiveStudioId() ?? "_default";
|
|
1770
1937
|
if (params.action === "begin") {
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1938
|
+
try {
|
|
1939
|
+
const result = await bridge.send("begin_transaction", {
|
|
1940
|
+
name: params.name
|
|
1941
|
+
});
|
|
1942
|
+
transactionState.set(studioId, true);
|
|
1943
|
+
return {
|
|
1944
|
+
content: [
|
|
1945
|
+
{ type: "text", text: `Transaction started: **${result.transactionId}**
|
|
1946
|
+
All subsequent writes will be grouped into one undo point. Call commit or rollback to finish.` }
|
|
1947
|
+
]
|
|
1948
|
+
};
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
transactionState.set(studioId, false);
|
|
1951
|
+
throw err;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
if (!transactionState.get(studioId)) {
|
|
1774
1955
|
return {
|
|
1775
1956
|
content: [
|
|
1776
|
-
{ type: "text", text: `
|
|
1777
|
-
All subsequent writes will be grouped into one undo point. Call commit or rollback to finish.` }
|
|
1957
|
+
{ type: "text", text: `No active transaction. Call \`begin\` first before calling \`${params.action}\`.` }
|
|
1778
1958
|
]
|
|
1779
1959
|
};
|
|
1780
1960
|
}
|
|
1781
1961
|
if (params.action === "commit") {
|
|
1782
|
-
|
|
1962
|
+
try {
|
|
1963
|
+
const result = await bridge.send("commit_transaction", {});
|
|
1964
|
+
return {
|
|
1965
|
+
content: [
|
|
1966
|
+
{ type: "text", text: `Transaction ${result.status}. All changes are now a single undo point.` }
|
|
1967
|
+
]
|
|
1968
|
+
};
|
|
1969
|
+
} finally {
|
|
1970
|
+
transactionState.set(studioId, false);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
try {
|
|
1974
|
+
const result = await bridge.send("rollback_transaction", {});
|
|
1783
1975
|
return {
|
|
1784
1976
|
content: [
|
|
1785
|
-
{ type: "text", text: `Transaction ${
|
|
1977
|
+
{ type: "text", text: `Transaction ${result.status}. All changes since begin have been reverted.` }
|
|
1786
1978
|
]
|
|
1787
1979
|
};
|
|
1980
|
+
} finally {
|
|
1981
|
+
transactionState.set(studioId, false);
|
|
1788
1982
|
}
|
|
1789
|
-
const result = await bridge.send("rollback_transaction", {});
|
|
1790
|
-
return {
|
|
1791
|
-
content: [
|
|
1792
|
-
{ type: "text", text: `Transaction ${result.status}. All changes since begin have been reverted.` }
|
|
1793
|
-
]
|
|
1794
|
-
};
|
|
1795
1983
|
}
|
|
1796
1984
|
);
|
|
1797
1985
|
}
|
|
@@ -1853,7 +2041,15 @@ ${lines.join("\n")}`;
|
|
|
1853
2041
|
}
|
|
1854
2042
|
},
|
|
1855
2043
|
async (params) => {
|
|
1856
|
-
|
|
2044
|
+
try {
|
|
2045
|
+
bridge.setActiveStudio(params.studioId);
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2048
|
+
return {
|
|
2049
|
+
content: [{ type: "text", text: message }],
|
|
2050
|
+
isError: true
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
1857
2053
|
const studios = bridge.getStudios();
|
|
1858
2054
|
const studio = studios.find((s) => s.studioId === params.studioId);
|
|
1859
2055
|
const name = studio?.placeName ?? "unknown";
|
|
@@ -1883,7 +2079,13 @@ function ensureDir() {
|
|
|
1883
2079
|
mkdirSync(BUILDS_DIR, { recursive: true });
|
|
1884
2080
|
}
|
|
1885
2081
|
}
|
|
2082
|
+
function validateBuildName(name) {
|
|
2083
|
+
if (!/^[\w-]+$/.test(name)) {
|
|
2084
|
+
throw new Error(`Invalid build name "${name}". Names must match /^[\\w-]+$/.`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
1886
2087
|
function saveBuild(name, root, description) {
|
|
2088
|
+
validateBuildName(name);
|
|
1887
2089
|
ensureDir();
|
|
1888
2090
|
const rootObj = root;
|
|
1889
2091
|
const data = {
|
|
@@ -1903,6 +2105,7 @@ function saveBuild(name, root, description) {
|
|
|
1903
2105
|
};
|
|
1904
2106
|
}
|
|
1905
2107
|
function loadBuild(name) {
|
|
2108
|
+
validateBuildName(name);
|
|
1906
2109
|
const filePath = join(BUILDS_DIR, `${name}.json`);
|
|
1907
2110
|
if (!existsSync(filePath)) {
|
|
1908
2111
|
throw new Error(`Build "${name}" not found. Use builds --action list to see available builds.`);
|
|
@@ -2027,7 +2230,7 @@ function register9(server, bridge) {
|
|
|
2027
2230
|
}
|
|
2028
2231
|
|
|
2029
2232
|
// src/tools/index.ts
|
|
2030
|
-
function registerAllTools(server, bridge, options = {}) {
|
|
2233
|
+
async function registerAllTools(server, bridge, options = {}) {
|
|
2031
2234
|
const mode = options.mode ?? "full";
|
|
2032
2235
|
register8(server, bridge);
|
|
2033
2236
|
register(server, bridge);
|
|
@@ -2044,10 +2247,20 @@ function registerAllTools(server, bridge, options = {}) {
|
|
|
2044
2247
|
registerReadOnly2(server, bridge);
|
|
2045
2248
|
}
|
|
2046
2249
|
if (options.withCloud) {
|
|
2047
|
-
|
|
2250
|
+
try {
|
|
2251
|
+
const mod = await import("./cloud-4AYJVND6.js");
|
|
2252
|
+
mod.register(server);
|
|
2253
|
+
} catch (err) {
|
|
2254
|
+
log.error("Failed to load cloud module:", err);
|
|
2255
|
+
}
|
|
2048
2256
|
}
|
|
2049
2257
|
if (options.withRojo) {
|
|
2050
|
-
|
|
2258
|
+
try {
|
|
2259
|
+
const mod = await import("./rojo-2CMKPTPS.js");
|
|
2260
|
+
mod.register(server);
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
log.error("Failed to load rojo module:", err);
|
|
2263
|
+
}
|
|
2051
2264
|
}
|
|
2052
2265
|
}
|
|
2053
2266
|
|
|
@@ -2088,7 +2301,7 @@ async function startServer(port = 3200, options = {}) {
|
|
|
2088
2301
|
withCloud: options.withCloud,
|
|
2089
2302
|
withRojo: options.withRojo
|
|
2090
2303
|
};
|
|
2091
|
-
registerAllTools(server, bridge, toolOptions);
|
|
2304
|
+
await registerAllTools(server, bridge, toolOptions);
|
|
2092
2305
|
bridge.on("studio-connected", (info) => {
|
|
2093
2306
|
log.info(
|
|
2094
2307
|
`Roblox Studio connected: ${info.studioId}` + (info.placeName ? ` (${info.placeName})` : "")
|
|
@@ -2104,12 +2317,16 @@ async function startServer(port = 3200, options = {}) {
|
|
|
2104
2317
|
const transport = new StdioServerTransport();
|
|
2105
2318
|
await server.connect(transport);
|
|
2106
2319
|
log.info("MCP server connected via stdio");
|
|
2320
|
+
let shuttingDown = false;
|
|
2107
2321
|
const shutdown = async () => {
|
|
2322
|
+
if (shuttingDown) return;
|
|
2323
|
+
shuttingDown = true;
|
|
2108
2324
|
log.info("Shutting down...");
|
|
2109
2325
|
await bridge.stop();
|
|
2110
2326
|
await server.close();
|
|
2111
2327
|
process.exit(0);
|
|
2112
2328
|
};
|
|
2329
|
+
bridge.on("shutdown", shutdown);
|
|
2113
2330
|
process.on("SIGINT", shutdown);
|
|
2114
2331
|
process.on("SIGTERM", shutdown);
|
|
2115
2332
|
}
|
|
@@ -2117,4 +2334,4 @@ async function startServer(port = 3200, options = {}) {
|
|
|
2117
2334
|
export {
|
|
2118
2335
|
startServer
|
|
2119
2336
|
};
|
|
2120
|
-
//# sourceMappingURL=chunk-
|
|
2337
|
+
//# sourceMappingURL=chunk-U4GZFDM3.js.map
|