@el4cteo/rbx-studio-mcp 0.8.0 → 0.8.2

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.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/apidump.js +24 -2
  3. package/dist/lib/apidump.js.map +1 -1
  4. package/dist/lib/livedata.js +24 -1
  5. package/dist/lib/livedata.js.map +1 -1
  6. package/dist/lib/liveops.js +79 -0
  7. package/dist/lib/liveops.js.map +1 -1
  8. package/dist/lib/monetization.js +119 -0
  9. package/dist/lib/monetization.js.map +1 -0
  10. package/dist/lib/opencloud.js +16 -6
  11. package/dist/lib/opencloud.js.map +1 -1
  12. package/dist/tools/audio.js +1 -1
  13. package/dist/tools/audio.js.map +1 -1
  14. package/dist/tools/device.js +8 -7
  15. package/dist/tools/device.js.map +1 -1
  16. package/dist/tools/discover.js +8 -1
  17. package/dist/tools/discover.js.map +1 -1
  18. package/dist/tools/perf.js +27 -5
  19. package/dist/tools/perf.js.map +1 -1
  20. package/dist/tools/playtest.js +35 -10
  21. package/dist/tools/playtest.js.map +1 -1
  22. package/dist/tools/terrain.js +2 -2
  23. package/dist/tools/terrain.js.map +1 -1
  24. package/dist/tools/universe.js +136 -11
  25. package/dist/tools/universe.js.map +1 -1
  26. package/dist/tools/world.js +30 -7
  27. package/dist/tools/world.js.map +1 -1
  28. package/package.json +2 -2
  29. package/plugin/src/ClientRelay.luau +20 -2
  30. package/plugin/src/Config.luau +1 -1
  31. package/plugin/src/Emulation.luau +44 -16
  32. package/plugin/src/ExecRuntime.luau +168 -161
  33. package/plugin/src/LogBuffer.luau +58 -3
  34. package/plugin/src/Phrase.luau +2 -0
  35. package/plugin/src/handlers/Assets.luau +27 -18
  36. package/plugin/src/handlers/Audio.luau +36 -15
  37. package/plugin/src/handlers/Character.luau +38 -23
  38. package/plugin/src/handlers/Data.luau +12 -1
  39. package/plugin/src/handlers/Device.luau +38 -27
  40. package/plugin/src/handlers/Generate.luau +8 -5
  41. package/plugin/src/handlers/Geometry.luau +7 -5
  42. package/plugin/src/handlers/Perf.luau +1 -0
  43. package/plugin/src/handlers/Playtest.luau +40 -3
  44. package/plugin/src/handlers/Terrain.luau +9 -24
  45. package/plugin/src/handlers/Viewport.luau +4 -4
  46. package/plugin/src/handlers/World.luau +17 -1
  47. package/scripts/test-tools.mjs +178 -0
@@ -148,6 +148,31 @@ process.stdout.write("playtest lock errors and instruction precedence: ok\n");
148
148
  }
149
149
  process.stdout.write("playtest runtime identity: ok\n");
150
150
 
151
+ // addPlayers goes to the running test's server and waits for the new players.
152
+ {
153
+ const originalSessions = context.bridge.sessions;
154
+ const originalCall = context.bridge.call;
155
+ let joined = 1;
156
+ const sent = [];
157
+ context.bridge.sessions = async () => ({list:[{studioId:"editor",context:"edit"},{studioId:"runtime",context:"playtest server"}]});
158
+ context.bridge.call = async (op, params, options) => {
159
+ sent.push({op: params.op, studioId: options.studioId, players: params.players});
160
+ if (params.op === "addPlayers") return {changed:true,state:{playerCount:joined}};
161
+ joined = 3;
162
+ return {changed:false,state:{playerCount:joined}};
163
+ };
164
+ const result = await playtest.handler(z.object(playtest.spec.inputSchema).parse({op:"addPlayers",players:2}));
165
+ assert.ok(!result.isError);
166
+ assert.deepEqual(sent[0], {op:"addPlayers",studioId:"runtime",players:2});
167
+ assert.match(result.content[0].text, /"playerCount": ?3/);
168
+ context.bridge.sessions = async () => ({list:[{studioId:"editor",context:"edit"}]});
169
+ const none = await playtest.handler(z.object(playtest.spec.inputSchema).parse({op:"addPlayers"}));
170
+ assert.equal(none.isError, true);
171
+ context.bridge.sessions = originalSessions;
172
+ context.bridge.call = originalCall;
173
+ }
174
+ process.stdout.write("playtest addPlayers routing: ok\n");
175
+
151
176
  // OFF affects simulation starts, not the ordinary edit-mode tool paths.
152
177
  const { registerDiscoverTools } = await import("../dist/tools/discover.js");
153
178
  const { registerScriptTools } = await import("../dist/tools/scripts.js");
@@ -257,3 +282,156 @@ process.stdout.write("client console schema and cursor forwarding: ok\n");
257
282
  assert.deepEqual([...upscaleNearest(Buffer.from([1,2,3,4,5,6]), 2, 1, 2).rgb], [1,2,3,1,2,3,4,5,6,4,5,6,1,2,3,1,2,3,4,5,6,4,5,6]);
258
283
  process.stdout.write("screenshot scaling: ok\n");
259
284
  }
285
+
286
+ // scene: a category of bare counts renders on one line; owned entries keep theirs.
287
+ {
288
+ const perf = registered.get("performance");
289
+ context.bridge.call = async () => ({
290
+ composition: {total: 12, unit: "instances", entries: [
291
+ {name: "Physics", depth: 1, size: 10},
292
+ {name: "Motor6D", depth: 2, size: 6},
293
+ {name: "Attachment", depth: 2, size: 4},
294
+ {name: "Misc", depth: 1, size: 2},
295
+ ]},
296
+ animationMemory: {total: 100, unit: "bytes", entries: [
297
+ {name: "Walk", depth: 1, size: 100, owners: ["Workspace.Npc.Animator"]},
298
+ ]},
299
+ });
300
+ const result = await perf.handler(z.object(perf.spec.inputSchema).parse({op: "scene"}));
301
+ const out = result.content[0].text;
302
+ assert.match(out, / Physics — 10 instances: Motor6D 6, Attachment 4\n/);
303
+ assert.match(out, / Misc — 2 instances/);
304
+ assert.match(out, / Walk — 100 bytes\n used by Workspace\.Npc\.Animator/);
305
+ process.stdout.write("scene compact rows: ok\n");
306
+ }
307
+
308
+ // inspect: children come back as compact "Name (Class)" strings.
309
+ {
310
+ const inspect = registered.get("inspect");
311
+ context.bridge.call = async () => ({
312
+ items: [{path: "Workspace.Npc", className: "Model", childCount: 2, properties: {Name: "Npc"},
313
+ children: [{name: "Head", className: "Part"}, {name: "Humanoid", className: "Humanoid"}]}],
314
+ failures: [],
315
+ });
316
+ const result = await inspect.handler(z.object(inspect.spec.inputSchema).parse({paths: ["Workspace.Npc"], properties: ["Name"]}));
317
+ assert.match(result.content[0].text, /"children": ?\["Head \(Part\)", ?"Humanoid"\]/);
318
+ process.stdout.write("inspect compact children: ok\n");
319
+ }
320
+
321
+ // universe servers/logs: routed to Server Management with the wildcard version,
322
+ // rendered as a table and as log blocks. Fetch is stubbed; nothing leaves.
323
+ {
324
+ const { registerUniverseTools } = await import("../dist/tools/universe.js");
325
+ registerUniverseTools(context);
326
+ const universe = registered.get("universe");
327
+ const saved = { fetch: globalThis.fetch, env: { ...process.env } };
328
+ Object.assign(process.env, { ROBLOX_API_KEY: "test-key", ROBLOX_USER_ID: "1", ROBLOX_UNIVERSE_ID: "10", ROBLOX_PLACE_ID: "20" });
329
+ context.bridge.call = async () => ({ placeId: 20 });
330
+ const urls = [];
331
+ globalThis.fetch = async (url) => {
332
+ urls.push(String(url));
333
+ if (String(url).includes("/universes/v1/places/")) return new Response(JSON.stringify({ universeId: 10 }), { status: 200 });
334
+ const body = String(url).includes("/logs")
335
+ ? { gameServerLogs: [{ messageTimestampMs: "0", severity: 3, message: "boom", stackTrace: "Script 'X', Line 4", context: "{\"id\":1}", skippedCount: 2 }], nextPageToken: null }
336
+ : { gameServers: [{ jobId: "job-1", status: "active", occupancy: 3, maxOccupancy: 10, uptime: "120s", frameRate: 59.7, memoryUsageBytes: 104857600, placeVersion: "12" }], nextPageToken: null };
337
+ return new Response(JSON.stringify(body), { status: 200 });
338
+ };
339
+ try {
340
+ const servers = await universe.handler(z.object(universe.spec.inputSchema).parse({ op: "servers" }));
341
+ assert.match(servers.content[0].text, /job-1 \| active \| 3\/10 \| 120s \| 60 \| 100 \| 12/);
342
+ assert.match(urls.find((u) => u.includes("game-servers")), /\/server-management\/v1\/universes\/10\/places\/20\/versions\/-\/game-servers\?/);
343
+ const logs = await universe.handler(z.object(universe.spec.inputSchema).parse({ op: "logs", jobId: "job-1", severity: "error", search: "boom" }));
344
+ const out = logs.content[0].text;
345
+ assert.match(out, /\[error\] boom \(\+2 similar\)\n Script 'X', Line 4\n context: \{"id":1\}/);
346
+ assert.equal(new URL(urls.find((u) => u.includes("/logs"))).searchParams.get("Filter"), 'severity == 3 && search == "boom"');
347
+ const missing = await universe.handler(z.object(universe.spec.inputSchema).parse({ op: "logs" }));
348
+ assert.equal(missing.isError, true);
349
+ } finally {
350
+ globalThis.fetch = saved.fetch;
351
+ for (const key of ["ROBLOX_API_KEY", "ROBLOX_USER_ID", "ROBLOX_UNIVERSE_ID", "ROBLOX_PLACE_ID"]) {
352
+ if (saved.env[key] === undefined) delete process.env[key]; else process.env[key] = saved.env[key];
353
+ }
354
+ }
355
+ process.stdout.write("universe servers and logs: ok\n");
356
+ }
357
+
358
+ // console: a structured log's context is shown under its line.
359
+ {
360
+ const consoleTool = registered.get("console");
361
+ context.bridge.call = async () => ({ items: [{ level: "info", message: "Bob leveled up", context: "{\"player\":\"Bob\"}" }], total: 1, dropped: 0, nextCursor: "c" });
362
+ const result = await consoleTool.handler(z.object(consoleTool.spec.inputSchema).parse({}));
363
+ assert.match(result.content[0].text, /\[info\] Bob leveled up\n context: \{"player":"Bob"\}/);
364
+ process.stdout.write("console structured context: ok\n");
365
+ }
366
+
367
+ // universe products/sell: lists both kinds, refuses a duplicate name, needs confirm.
368
+ {
369
+ const universe = registered.get("universe");
370
+ const saved = { fetch: globalThis.fetch, env: { ...process.env } };
371
+ Object.assign(process.env, { ROBLOX_API_KEY: "test-key", ROBLOX_USER_ID: "1", ROBLOX_UNIVERSE_ID: "10", ROBLOX_PLACE_ID: "20" });
372
+ context.bridge.call = async () => ({ placeId: 20 });
373
+ const sent = [];
374
+ globalThis.fetch = async (url, init) => {
375
+ const href = String(url);
376
+ sent.push({ href, method: init?.method ?? "GET", body: init?.body });
377
+ if (href.includes("/universes/v1/places/")) return new Response(JSON.stringify({ universeId: 10 }), { status: 200 });
378
+ if (href.includes("developer-products/creator")) return new Response(JSON.stringify({ developerProducts: [{ productId: 5, name: "100 Coins", isForSale: true, priceInformation: { defaultPriceInRobux: 25 } }] }), { status: 200 });
379
+ if (href.includes("game-passes/creator")) return new Response(JSON.stringify({ gamePasses: [{ gamePassId: 7, name: "VIP", isForSale: false }] }), { status: 200 });
380
+ if (init?.method === "POST") return new Response(JSON.stringify({ productId: 6, name: "500 Coins", isForSale: true, priceInformation: { defaultPriceInRobux: 99 } }), { status: 200 });
381
+ return new Response("", { status: 200 });
382
+ };
383
+ const parse = (args) => z.object(universe.spec.inputSchema).parse(args);
384
+ try {
385
+ const list = (await universe.handler(parse({ op: "products" }))).content[0].text;
386
+ assert.match(list, /product \| 5 \| 100 Coins \| 25 \| true/);
387
+ assert.match(list, /pass \| 7 \| VIP \| \| false/);
388
+ const unconfirmed = await universe.handler(parse({ op: "sell", kind: "product", name: "500 Coins", price: 99 }));
389
+ assert.match(unconfirmed.content[0].text, /NEEDS_CONFIRM/);
390
+ const duplicate = await universe.handler(parse({ op: "sell", kind: "product", name: "100 coins", price: 25, confirm: true }));
391
+ assert.match(duplicate.content[0].text, /ALREADY_EXISTS\].*id 5/);
392
+ const created = await universe.handler(parse({ op: "sell", kind: "product", name: "500 Coins", price: 99, forSale: true, confirm: true }));
393
+ assert.match(created.content[0].text, /"id": ?6/);
394
+ const post = sent.find((entry) => entry.method === "POST");
395
+ assert.ok(post.href.endsWith("/developer-products/v2/universes/10/developer-products"));
396
+ assert.equal(post.body.get("price"), "99");
397
+ assert.equal(post.body.get("isForSale"), "true");
398
+ const updated = await universe.handler(parse({ op: "sell", kind: "pass", itemId: "7", price: 150, confirm: true }));
399
+ assert.match(updated.content[0].text, /"action": ?"updated"/);
400
+ assert.ok(sent.some((entry) => entry.method === "PATCH" && entry.href.endsWith("/game-passes/v1/universes/10/game-passes/7")));
401
+ } finally {
402
+ globalThis.fetch = saved.fetch;
403
+ for (const key of ["ROBLOX_API_KEY", "ROBLOX_USER_ID", "ROBLOX_UNIVERSE_ID", "ROBLOX_PLACE_ID"]) {
404
+ if (saved.env[key] === undefined) delete process.env[key]; else process.env[key] = saved.env[key];
405
+ }
406
+ }
407
+ process.stdout.write("universe products and sell: ok\n");
408
+ }
409
+
410
+ // datastore live set: keeps the entry's users and attributes, and sends its etag.
411
+ {
412
+ const { registerDataTools } = await import("../dist/tools/data.js");
413
+ registerDataTools(context);
414
+ const datastore = registered.get("datastore");
415
+ const saved = { fetch: globalThis.fetch, env: { ...process.env } };
416
+ Object.assign(process.env, { ROBLOX_API_KEY: "test-key", ROBLOX_USER_ID: "1", ROBLOX_UNIVERSE_ID: "10", ROBLOX_PLACE_ID: "20" });
417
+ context.bridge.call = async () => ({ placeId: 20 });
418
+ let patched;
419
+ globalThis.fetch = async (url, init) => {
420
+ const href = String(url);
421
+ if (href.includes("/universes/v1/places/")) return new Response(JSON.stringify({ universeId: 10 }), { status: 200 });
422
+ if ((init?.method ?? "GET") === "GET") return new Response(JSON.stringify({ value: { coins: 1 }, users: ["users/42"], attributes: { lock: "s1" }, etag: "e1" }), { status: 200 });
423
+ patched = JSON.parse(init.body);
424
+ return new Response(JSON.stringify({ revisionId: "r2" }), { status: 200 });
425
+ };
426
+ try {
427
+ const result = await datastore.handler(z.object(datastore.spec.inputSchema).parse({ target: "live", op: "set", store: "Players", key: "42", value: "{\"coins\":5}", confirm: true }));
428
+ assert.ok(!result.isError, result.content[0].text);
429
+ assert.deepEqual(patched, { value: { coins: 5 }, users: ["users/42"], attributes: { lock: "s1" }, etag: "e1" });
430
+ } finally {
431
+ globalThis.fetch = saved.fetch;
432
+ for (const key of ["ROBLOX_API_KEY", "ROBLOX_USER_ID", "ROBLOX_UNIVERSE_ID", "ROBLOX_PLACE_ID"]) {
433
+ if (saved.env[key] === undefined) delete process.env[key]; else process.env[key] = saved.env[key];
434
+ }
435
+ }
436
+ process.stdout.write("datastore live set keeps users and attributes: ok\n");
437
+ }