@chrrxs/robloxstudio-mcp-inspector 2.12.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -43,18 +43,25 @@ var init_bridge_service = __esm({
43
43
  requestTimeout = 3e4;
44
44
  registerInstance(input) {
45
45
  const { pluginSessionId, instanceId, role } = input;
46
+ const prior = this.instances.get(pluginSessionId);
46
47
  let assignedRole = role;
47
48
  if (role === "client") {
48
- const used = /* @__PURE__ */ new Set();
49
- for (const inst of this.instances.values()) {
50
- const match = inst.role.match(/^client-(\d+)$/);
51
- if (match)
52
- used.add(Number(match[1]));
49
+ if (prior && prior.instanceId === instanceId && prior.role.match(/^client-\d+$/)) {
50
+ assignedRole = prior.role;
51
+ } else {
52
+ const used = /* @__PURE__ */ new Set();
53
+ for (const inst of this.instances.values()) {
54
+ if (inst.instanceId !== instanceId || inst.pluginSessionId === pluginSessionId)
55
+ continue;
56
+ const match = inst.role.match(/^client-(\d+)$/);
57
+ if (match)
58
+ used.add(Number(match[1]));
59
+ }
60
+ let idx = 1;
61
+ while (used.has(idx))
62
+ idx++;
63
+ assignedRole = `client-${idx}`;
53
64
  }
54
- let idx = 1;
55
- while (used.has(idx))
56
- idx++;
57
- assignedRole = `client-${idx}`;
58
65
  }
59
66
  const existing = Array.from(this.instances.values()).find((i) => i.instanceId === instanceId && i.role === assignedRole && i.pluginSessionId !== pluginSessionId);
60
67
  if (existing) {
@@ -76,7 +83,7 @@ var init_bridge_service = __esm({
76
83
  dataModelName: input.dataModelName ?? "",
77
84
  isRunning: input.isRunning ?? false,
78
85
  lastActivity: Date.now(),
79
- connectedAt: Date.now()
86
+ connectedAt: prior?.connectedAt ?? Date.now()
80
87
  });
81
88
  return { ok: true, assignedRole, instanceId };
82
89
  }
@@ -214,7 +221,7 @@ var init_bridge_service = __esm({
214
221
  }
215
222
  if (distinctInstanceIds.size > 1) {
216
223
  const errorCode = role ? "ambiguous_target" : "multiple_instances_connected";
217
- const msg = role ? `target=${role} is ambiguous: multiple places have this role. Pass instance_id.` : "Multiple Studio places are connected. Pass instance_id to disambiguate.";
224
+ const msg = role ? `target=${role} is ambiguous because multiple Studio places are connected. Pass instance_id to choose a place.` : "Multiple Studio places are connected. Pass instance_id to disambiguate.";
218
225
  return { ok: false, error: { code: errorCode, message: msg, data: errorData } };
219
226
  }
220
227
  const onlyInstanceId = instances[0].instanceId;
@@ -236,6 +243,7 @@ var init_bridge_service = __esm({
236
243
  targetInstanceId,
237
244
  targetRole,
238
245
  timestamp: Date.now(),
246
+ inFlight: false,
239
247
  resolve: resolve2,
240
248
  reject,
241
249
  timeoutId
@@ -250,11 +258,14 @@ var init_bridge_service = __esm({
250
258
  continue;
251
259
  if (request.targetRole !== callerRole)
252
260
  continue;
261
+ if (request.inFlight)
262
+ continue;
253
263
  if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
254
264
  oldestRequest = request;
255
265
  }
256
266
  }
257
267
  if (oldestRequest) {
268
+ oldestRequest.inFlight = true;
258
269
  return {
259
270
  requestId: oldestRequest.id,
260
271
  request: {
@@ -677,6 +688,11 @@ var init_http_server = __esm({
677
688
  start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers, body.instance_id),
678
689
  stop_playtest: (tools, body) => tools.stopPlaytest(body.instance_id),
679
690
  get_playtest_output: (tools, body) => tools.getPlaytestOutput(body.target, body.instance_id),
691
+ multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id),
692
+ multiplayer_test_state: (tools, body) => tools.multiplayerTestState(body.instance_id),
693
+ multiplayer_test_add_players: (tools, body) => tools.multiplayerTestAddPlayers(body.numPlayers, body.timeout, body.instance_id),
694
+ multiplayer_test_leave_client: (tools, body) => tools.multiplayerTestLeaveClient(body.target, body.timeout, body.instance_id),
695
+ multiplayer_test_end: (tools, body) => tools.multiplayerTestEnd(body.value, body.timeout, body.instance_id),
680
696
  get_runtime_logs: (tools, body) => tools.getRuntimeLogs(body.target, body.since, body.tail, body.filter, body.instance_id),
681
697
  get_connected_instances: (tools) => tools.getConnectedInstances(),
682
698
  export_build: (tools, body) => tools.exportBuild(body.instancePath, body.outputId, body.style, body.instance_id),
@@ -700,9 +716,9 @@ var init_http_server = __esm({
700
716
  compare_instances: (tools, body) => tools.compareInstances(body.instancePathA, body.instancePathB, body.instance_id),
701
717
  get_output_log: (tools, body) => tools.getOutputLog(body.maxEntries, body.messageType, body.instance_id),
702
718
  bulk_set_attributes: (tools, body) => tools.bulkSetAttributes(body.instancePath, body.attributes, body.instance_id),
703
- capture_screenshot: (tools, body) => tools.captureScreenshot(body.instance_id),
719
+ capture_screenshot: (tools, body) => tools.captureScreenshot(body.instance_id, body.format, body.quality),
704
720
  simulate_mouse_input: (tools, body) => tools.simulateMouseInput(body.action, body.x, body.y, body.button, body.scrollDirection, body.target, body.instance_id),
705
- simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.target, body.instance_id),
721
+ simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.text, body.target, body.instance_id),
706
722
  character_navigation: (tools, body) => tools.characterNavigation(body.position, body.instancePath, body.waitForCompletion, body.timeout, body.target, body.instance_id),
707
723
  get_memory_breakdown: (tools, body) => tools.getMemoryBreakdown(body.target, body.tags, body.instance_id),
708
724
  export_rbxm: (tools, body) => tools.exportRbxm(body.instance_paths, body.output_path, body.target, body.instance_id),
@@ -1468,6 +1484,963 @@ var init_roblox_cookie_client = __esm({
1468
1484
  }
1469
1485
  });
1470
1486
 
1487
+ // ../core/dist/jpeg-encoder.js
1488
+ function rgbaToJpeg(rgba, width, height, quality = 80) {
1489
+ if (width <= 0 || height <= 0)
1490
+ throw new Error(`Invalid JPEG dimensions: ${width}x${height}`);
1491
+ const expected = width * height * 4;
1492
+ if (rgba.length < expected)
1493
+ throw new Error(`Buffer too small: got ${rgba.length}, need ${expected}`);
1494
+ const encoder = new JpegEncoder(quality);
1495
+ return encoder.encode(rgba, width, height);
1496
+ }
1497
+ var ZIGZAG, STD_DC_LUMINANCE_NRCODES, STD_DC_LUMINANCE_VALUES, STD_AC_LUMINANCE_NRCODES, STD_AC_LUMINANCE_VALUES, STD_DC_CHROMINANCE_NRCODES, STD_DC_CHROMINANCE_VALUES, STD_AC_CHROMINANCE_NRCODES, STD_AC_CHROMINANCE_VALUES, JpegEncoder;
1498
+ var init_jpeg_encoder = __esm({
1499
+ "../core/dist/jpeg-encoder.js"() {
1500
+ "use strict";
1501
+ ZIGZAG = [
1502
+ 0,
1503
+ 1,
1504
+ 5,
1505
+ 6,
1506
+ 14,
1507
+ 15,
1508
+ 27,
1509
+ 28,
1510
+ 2,
1511
+ 4,
1512
+ 7,
1513
+ 13,
1514
+ 16,
1515
+ 26,
1516
+ 29,
1517
+ 42,
1518
+ 3,
1519
+ 8,
1520
+ 12,
1521
+ 17,
1522
+ 25,
1523
+ 30,
1524
+ 41,
1525
+ 43,
1526
+ 9,
1527
+ 11,
1528
+ 18,
1529
+ 24,
1530
+ 31,
1531
+ 40,
1532
+ 44,
1533
+ 53,
1534
+ 10,
1535
+ 19,
1536
+ 23,
1537
+ 32,
1538
+ 39,
1539
+ 45,
1540
+ 52,
1541
+ 54,
1542
+ 20,
1543
+ 22,
1544
+ 33,
1545
+ 38,
1546
+ 46,
1547
+ 51,
1548
+ 55,
1549
+ 60,
1550
+ 21,
1551
+ 34,
1552
+ 37,
1553
+ 47,
1554
+ 50,
1555
+ 56,
1556
+ 59,
1557
+ 61,
1558
+ 35,
1559
+ 36,
1560
+ 48,
1561
+ 49,
1562
+ 57,
1563
+ 58,
1564
+ 62,
1565
+ 63
1566
+ ];
1567
+ STD_DC_LUMINANCE_NRCODES = [0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0];
1568
+ STD_DC_LUMINANCE_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
1569
+ STD_AC_LUMINANCE_NRCODES = [0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125];
1570
+ STD_AC_LUMINANCE_VALUES = [
1571
+ 1,
1572
+ 2,
1573
+ 3,
1574
+ 0,
1575
+ 4,
1576
+ 17,
1577
+ 5,
1578
+ 18,
1579
+ 33,
1580
+ 49,
1581
+ 65,
1582
+ 6,
1583
+ 19,
1584
+ 81,
1585
+ 97,
1586
+ 7,
1587
+ 34,
1588
+ 113,
1589
+ 20,
1590
+ 50,
1591
+ 129,
1592
+ 145,
1593
+ 161,
1594
+ 8,
1595
+ 35,
1596
+ 66,
1597
+ 177,
1598
+ 193,
1599
+ 21,
1600
+ 82,
1601
+ 209,
1602
+ 240,
1603
+ 36,
1604
+ 51,
1605
+ 98,
1606
+ 114,
1607
+ 130,
1608
+ 9,
1609
+ 10,
1610
+ 22,
1611
+ 23,
1612
+ 24,
1613
+ 25,
1614
+ 26,
1615
+ 37,
1616
+ 38,
1617
+ 39,
1618
+ 40,
1619
+ 41,
1620
+ 42,
1621
+ 52,
1622
+ 53,
1623
+ 54,
1624
+ 55,
1625
+ 56,
1626
+ 57,
1627
+ 58,
1628
+ 67,
1629
+ 68,
1630
+ 69,
1631
+ 70,
1632
+ 71,
1633
+ 72,
1634
+ 73,
1635
+ 74,
1636
+ 83,
1637
+ 84,
1638
+ 85,
1639
+ 86,
1640
+ 87,
1641
+ 88,
1642
+ 89,
1643
+ 90,
1644
+ 99,
1645
+ 100,
1646
+ 101,
1647
+ 102,
1648
+ 103,
1649
+ 104,
1650
+ 105,
1651
+ 106,
1652
+ 115,
1653
+ 116,
1654
+ 117,
1655
+ 118,
1656
+ 119,
1657
+ 120,
1658
+ 121,
1659
+ 122,
1660
+ 131,
1661
+ 132,
1662
+ 133,
1663
+ 134,
1664
+ 135,
1665
+ 136,
1666
+ 137,
1667
+ 138,
1668
+ 146,
1669
+ 147,
1670
+ 148,
1671
+ 149,
1672
+ 150,
1673
+ 151,
1674
+ 152,
1675
+ 153,
1676
+ 154,
1677
+ 162,
1678
+ 163,
1679
+ 164,
1680
+ 165,
1681
+ 166,
1682
+ 167,
1683
+ 168,
1684
+ 169,
1685
+ 170,
1686
+ 178,
1687
+ 179,
1688
+ 180,
1689
+ 181,
1690
+ 182,
1691
+ 183,
1692
+ 184,
1693
+ 185,
1694
+ 186,
1695
+ 194,
1696
+ 195,
1697
+ 196,
1698
+ 197,
1699
+ 198,
1700
+ 199,
1701
+ 200,
1702
+ 201,
1703
+ 202,
1704
+ 210,
1705
+ 211,
1706
+ 212,
1707
+ 213,
1708
+ 214,
1709
+ 215,
1710
+ 216,
1711
+ 217,
1712
+ 218,
1713
+ 225,
1714
+ 226,
1715
+ 227,
1716
+ 228,
1717
+ 229,
1718
+ 230,
1719
+ 231,
1720
+ 232,
1721
+ 233,
1722
+ 234,
1723
+ 241,
1724
+ 242,
1725
+ 243,
1726
+ 244,
1727
+ 245,
1728
+ 246,
1729
+ 247,
1730
+ 248,
1731
+ 249,
1732
+ 250
1733
+ ];
1734
+ STD_DC_CHROMINANCE_NRCODES = [0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0];
1735
+ STD_DC_CHROMINANCE_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
1736
+ STD_AC_CHROMINANCE_NRCODES = [0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119];
1737
+ STD_AC_CHROMINANCE_VALUES = [
1738
+ 0,
1739
+ 1,
1740
+ 2,
1741
+ 3,
1742
+ 17,
1743
+ 4,
1744
+ 5,
1745
+ 33,
1746
+ 49,
1747
+ 6,
1748
+ 18,
1749
+ 65,
1750
+ 81,
1751
+ 7,
1752
+ 97,
1753
+ 113,
1754
+ 19,
1755
+ 34,
1756
+ 50,
1757
+ 129,
1758
+ 8,
1759
+ 20,
1760
+ 66,
1761
+ 145,
1762
+ 161,
1763
+ 177,
1764
+ 193,
1765
+ 9,
1766
+ 35,
1767
+ 51,
1768
+ 82,
1769
+ 240,
1770
+ 21,
1771
+ 98,
1772
+ 114,
1773
+ 209,
1774
+ 10,
1775
+ 22,
1776
+ 36,
1777
+ 52,
1778
+ 225,
1779
+ 37,
1780
+ 241,
1781
+ 23,
1782
+ 24,
1783
+ 25,
1784
+ 26,
1785
+ 38,
1786
+ 39,
1787
+ 40,
1788
+ 41,
1789
+ 42,
1790
+ 53,
1791
+ 54,
1792
+ 55,
1793
+ 56,
1794
+ 57,
1795
+ 58,
1796
+ 67,
1797
+ 68,
1798
+ 69,
1799
+ 70,
1800
+ 71,
1801
+ 72,
1802
+ 73,
1803
+ 74,
1804
+ 83,
1805
+ 84,
1806
+ 85,
1807
+ 86,
1808
+ 87,
1809
+ 88,
1810
+ 89,
1811
+ 90,
1812
+ 99,
1813
+ 100,
1814
+ 101,
1815
+ 102,
1816
+ 103,
1817
+ 104,
1818
+ 105,
1819
+ 106,
1820
+ 115,
1821
+ 116,
1822
+ 117,
1823
+ 118,
1824
+ 119,
1825
+ 120,
1826
+ 121,
1827
+ 122,
1828
+ 130,
1829
+ 131,
1830
+ 132,
1831
+ 133,
1832
+ 134,
1833
+ 135,
1834
+ 136,
1835
+ 137,
1836
+ 138,
1837
+ 146,
1838
+ 147,
1839
+ 148,
1840
+ 149,
1841
+ 150,
1842
+ 151,
1843
+ 152,
1844
+ 153,
1845
+ 154,
1846
+ 162,
1847
+ 163,
1848
+ 164,
1849
+ 165,
1850
+ 166,
1851
+ 167,
1852
+ 168,
1853
+ 169,
1854
+ 170,
1855
+ 178,
1856
+ 179,
1857
+ 180,
1858
+ 181,
1859
+ 182,
1860
+ 183,
1861
+ 184,
1862
+ 185,
1863
+ 186,
1864
+ 194,
1865
+ 195,
1866
+ 196,
1867
+ 197,
1868
+ 198,
1869
+ 199,
1870
+ 200,
1871
+ 201,
1872
+ 202,
1873
+ 210,
1874
+ 211,
1875
+ 212,
1876
+ 213,
1877
+ 214,
1878
+ 215,
1879
+ 216,
1880
+ 217,
1881
+ 218,
1882
+ 226,
1883
+ 227,
1884
+ 228,
1885
+ 229,
1886
+ 230,
1887
+ 231,
1888
+ 232,
1889
+ 233,
1890
+ 234,
1891
+ 242,
1892
+ 243,
1893
+ 244,
1894
+ 245,
1895
+ 246,
1896
+ 247,
1897
+ 248,
1898
+ 249,
1899
+ 250
1900
+ ];
1901
+ JpegEncoder = class {
1902
+ YTable = new Int32Array(64);
1903
+ UVTable = new Int32Array(64);
1904
+ fdtbl_Y = new Float32Array(64);
1905
+ fdtbl_UV = new Float32Array(64);
1906
+ YDC_HT;
1907
+ UVDC_HT;
1908
+ YAC_HT;
1909
+ UVAC_HT;
1910
+ bitcode = new Array(65535);
1911
+ category = new Int32Array(65535);
1912
+ outputfDCTQuant = new Float32Array(64);
1913
+ DU = new Int32Array(64);
1914
+ // RGB->YUV lookup tables
1915
+ RGB_YUV_TABLE = new Int32Array(2048);
1916
+ byteout = [];
1917
+ bytenew = 0;
1918
+ bytepos = 7;
1919
+ constructor(quality) {
1920
+ this.initHuffmanTbl();
1921
+ this.initCategoryNumber();
1922
+ this.initRGBYUVTable();
1923
+ this.setQuality(quality);
1924
+ }
1925
+ initQuantTables(sf) {
1926
+ const YQT = [
1927
+ 16,
1928
+ 11,
1929
+ 10,
1930
+ 16,
1931
+ 24,
1932
+ 40,
1933
+ 51,
1934
+ 61,
1935
+ 12,
1936
+ 12,
1937
+ 14,
1938
+ 19,
1939
+ 26,
1940
+ 58,
1941
+ 60,
1942
+ 55,
1943
+ 14,
1944
+ 13,
1945
+ 16,
1946
+ 24,
1947
+ 40,
1948
+ 57,
1949
+ 69,
1950
+ 56,
1951
+ 14,
1952
+ 17,
1953
+ 22,
1954
+ 29,
1955
+ 51,
1956
+ 87,
1957
+ 80,
1958
+ 62,
1959
+ 18,
1960
+ 22,
1961
+ 37,
1962
+ 56,
1963
+ 68,
1964
+ 109,
1965
+ 103,
1966
+ 77,
1967
+ 24,
1968
+ 35,
1969
+ 55,
1970
+ 64,
1971
+ 81,
1972
+ 104,
1973
+ 113,
1974
+ 92,
1975
+ 49,
1976
+ 64,
1977
+ 78,
1978
+ 87,
1979
+ 103,
1980
+ 121,
1981
+ 120,
1982
+ 101,
1983
+ 72,
1984
+ 92,
1985
+ 95,
1986
+ 98,
1987
+ 112,
1988
+ 100,
1989
+ 103,
1990
+ 99
1991
+ ];
1992
+ for (let i = 0; i < 64; i++) {
1993
+ let t = Math.floor((YQT[i] * sf + 50) / 100);
1994
+ if (t < 1)
1995
+ t = 1;
1996
+ else if (t > 255)
1997
+ t = 255;
1998
+ this.YTable[ZIGZAG[i]] = t;
1999
+ }
2000
+ const UVQT = [
2001
+ 17,
2002
+ 18,
2003
+ 24,
2004
+ 47,
2005
+ 99,
2006
+ 99,
2007
+ 99,
2008
+ 99,
2009
+ 18,
2010
+ 21,
2011
+ 26,
2012
+ 66,
2013
+ 99,
2014
+ 99,
2015
+ 99,
2016
+ 99,
2017
+ 24,
2018
+ 26,
2019
+ 56,
2020
+ 99,
2021
+ 99,
2022
+ 99,
2023
+ 99,
2024
+ 99,
2025
+ 47,
2026
+ 66,
2027
+ 99,
2028
+ 99,
2029
+ 99,
2030
+ 99,
2031
+ 99,
2032
+ 99,
2033
+ 99,
2034
+ 99,
2035
+ 99,
2036
+ 99,
2037
+ 99,
2038
+ 99,
2039
+ 99,
2040
+ 99,
2041
+ 99,
2042
+ 99,
2043
+ 99,
2044
+ 99,
2045
+ 99,
2046
+ 99,
2047
+ 99,
2048
+ 99,
2049
+ 99,
2050
+ 99,
2051
+ 99,
2052
+ 99,
2053
+ 99,
2054
+ 99,
2055
+ 99,
2056
+ 99,
2057
+ 99,
2058
+ 99,
2059
+ 99,
2060
+ 99,
2061
+ 99,
2062
+ 99,
2063
+ 99,
2064
+ 99
2065
+ ];
2066
+ for (let j = 0; j < 64; j++) {
2067
+ let u = Math.floor((UVQT[j] * sf + 50) / 100);
2068
+ if (u < 1)
2069
+ u = 1;
2070
+ else if (u > 255)
2071
+ u = 255;
2072
+ this.UVTable[ZIGZAG[j]] = u;
2073
+ }
2074
+ const aasf = [
2075
+ 1,
2076
+ 1.387039845,
2077
+ 1.306562965,
2078
+ 1.175875602,
2079
+ 1,
2080
+ 0.785694958,
2081
+ 0.5411961,
2082
+ 0.275899379
2083
+ ];
2084
+ let k = 0;
2085
+ for (let row = 0; row < 8; row++) {
2086
+ for (let col = 0; col < 8; col++) {
2087
+ this.fdtbl_Y[k] = 1 / (this.YTable[ZIGZAG[k]] * aasf[row] * aasf[col] * 8);
2088
+ this.fdtbl_UV[k] = 1 / (this.UVTable[ZIGZAG[k]] * aasf[row] * aasf[col] * 8);
2089
+ k++;
2090
+ }
2091
+ }
2092
+ }
2093
+ computeHuffmanTbl(nrcodes, std_table) {
2094
+ let codevalue = 0;
2095
+ let pos_in_table = 0;
2096
+ const HT = [];
2097
+ for (let k = 1; k <= 16; k++) {
2098
+ for (let j = 1; j <= nrcodes[k]; j++) {
2099
+ HT[std_table[pos_in_table]] = { code: codevalue, length: k };
2100
+ pos_in_table++;
2101
+ codevalue++;
2102
+ }
2103
+ codevalue *= 2;
2104
+ }
2105
+ return HT;
2106
+ }
2107
+ initHuffmanTbl() {
2108
+ this.YDC_HT = this.computeHuffmanTbl(STD_DC_LUMINANCE_NRCODES, STD_DC_LUMINANCE_VALUES);
2109
+ this.UVDC_HT = this.computeHuffmanTbl(STD_DC_CHROMINANCE_NRCODES, STD_DC_CHROMINANCE_VALUES);
2110
+ this.YAC_HT = this.computeHuffmanTbl(STD_AC_LUMINANCE_NRCODES, STD_AC_LUMINANCE_VALUES);
2111
+ this.UVAC_HT = this.computeHuffmanTbl(STD_AC_CHROMINANCE_NRCODES, STD_AC_CHROMINANCE_VALUES);
2112
+ }
2113
+ initCategoryNumber() {
2114
+ let nrlower = 1;
2115
+ let nrupper = 2;
2116
+ for (let cat = 1; cat <= 15; cat++) {
2117
+ for (let nr = nrlower; nr < nrupper; nr++) {
2118
+ this.category[32767 + nr] = cat;
2119
+ this.bitcode[32767 + nr] = { length: cat, code: nr };
2120
+ }
2121
+ for (let nrneg = -(nrupper - 1); nrneg <= -nrlower; nrneg++) {
2122
+ this.category[32767 + nrneg] = cat;
2123
+ this.bitcode[32767 + nrneg] = { length: cat, code: nrupper - 1 + nrneg };
2124
+ }
2125
+ nrlower <<= 1;
2126
+ nrupper <<= 1;
2127
+ }
2128
+ }
2129
+ initRGBYUVTable() {
2130
+ for (let i = 0; i < 256; i++) {
2131
+ this.RGB_YUV_TABLE[i] = 19595 * i;
2132
+ this.RGB_YUV_TABLE[i + 256 >> 0] = 38470 * i;
2133
+ this.RGB_YUV_TABLE[i + 512 >> 0] = 7471 * i + 32768;
2134
+ this.RGB_YUV_TABLE[i + 768 >> 0] = -11059 * i;
2135
+ this.RGB_YUV_TABLE[i + 1024 >> 0] = -21709 * i;
2136
+ this.RGB_YUV_TABLE[i + 1280 >> 0] = 32768 * i + 8421375;
2137
+ this.RGB_YUV_TABLE[i + 1536 >> 0] = -27439 * i;
2138
+ this.RGB_YUV_TABLE[i + 1792 >> 0] = -5329 * i;
2139
+ }
2140
+ }
2141
+ setQuality(quality) {
2142
+ let q = quality;
2143
+ if (q <= 0)
2144
+ q = 1;
2145
+ if (q > 100)
2146
+ q = 100;
2147
+ const sf = q < 50 ? Math.floor(5e3 / q) : Math.floor(200 - q * 2);
2148
+ this.initQuantTables(sf);
2149
+ }
2150
+ writeBits(bs) {
2151
+ const value = bs.code;
2152
+ let posval = bs.length - 1;
2153
+ while (posval >= 0) {
2154
+ if (value & 1 << posval) {
2155
+ this.bytenew |= 1 << this.bytepos;
2156
+ }
2157
+ posval--;
2158
+ this.bytepos--;
2159
+ if (this.bytepos < 0) {
2160
+ if (this.bytenew === 255) {
2161
+ this.writeByte(255);
2162
+ this.writeByte(0);
2163
+ } else {
2164
+ this.writeByte(this.bytenew);
2165
+ }
2166
+ this.bytepos = 7;
2167
+ this.bytenew = 0;
2168
+ }
2169
+ }
2170
+ }
2171
+ writeByte(value) {
2172
+ this.byteout.push(value & 255);
2173
+ }
2174
+ writeWord(value) {
2175
+ this.writeByte(value >> 8 & 255);
2176
+ this.writeByte(value & 255);
2177
+ }
2178
+ fDCTQuant(data, fdtbl) {
2179
+ let d0, d1, d2, d3, d4, d5, d6, d7;
2180
+ let dataOff = 0;
2181
+ const I8 = 8;
2182
+ const I64 = 64;
2183
+ for (let i = 0; i < I8; ++i) {
2184
+ d0 = data[dataOff];
2185
+ d1 = data[dataOff + 1];
2186
+ d2 = data[dataOff + 2];
2187
+ d3 = data[dataOff + 3];
2188
+ d4 = data[dataOff + 4];
2189
+ d5 = data[dataOff + 5];
2190
+ d6 = data[dataOff + 6];
2191
+ d7 = data[dataOff + 7];
2192
+ const tmp0 = d0 + d7;
2193
+ const tmp7 = d0 - d7;
2194
+ const tmp1 = d1 + d6;
2195
+ const tmp6 = d1 - d6;
2196
+ const tmp2 = d2 + d5;
2197
+ const tmp5 = d2 - d5;
2198
+ const tmp3 = d3 + d4;
2199
+ const tmp4 = d3 - d4;
2200
+ let tmp10 = tmp0 + tmp3;
2201
+ const tmp13 = tmp0 - tmp3;
2202
+ let tmp11 = tmp1 + tmp2;
2203
+ let tmp12 = tmp1 - tmp2;
2204
+ data[dataOff] = tmp10 + tmp11;
2205
+ data[dataOff + 4] = tmp10 - tmp11;
2206
+ const z1 = (tmp12 + tmp13) * 0.707106781;
2207
+ data[dataOff + 2] = tmp13 + z1;
2208
+ data[dataOff + 6] = tmp13 - z1;
2209
+ tmp10 = tmp4 + tmp5;
2210
+ tmp11 = tmp5 + tmp6;
2211
+ tmp12 = tmp6 + tmp7;
2212
+ const z5 = (tmp10 - tmp12) * 0.382683433;
2213
+ const z2 = 0.5411961 * tmp10 + z5;
2214
+ const z4 = 1.306562965 * tmp12 + z5;
2215
+ const z3 = tmp11 * 0.707106781;
2216
+ const z11 = tmp7 + z3;
2217
+ const z13 = tmp7 - z3;
2218
+ data[dataOff + 5] = z13 + z2;
2219
+ data[dataOff + 3] = z13 - z2;
2220
+ data[dataOff + 1] = z11 + z4;
2221
+ data[dataOff + 7] = z11 - z4;
2222
+ dataOff += 8;
2223
+ }
2224
+ dataOff = 0;
2225
+ for (let i = 0; i < I8; ++i) {
2226
+ d0 = data[dataOff];
2227
+ d1 = data[dataOff + 8];
2228
+ d2 = data[dataOff + 16];
2229
+ d3 = data[dataOff + 24];
2230
+ d4 = data[dataOff + 32];
2231
+ d5 = data[dataOff + 40];
2232
+ d6 = data[dataOff + 48];
2233
+ d7 = data[dataOff + 56];
2234
+ const tmp0p2 = d0 + d7;
2235
+ const tmp7p2 = d0 - d7;
2236
+ const tmp1p2 = d1 + d6;
2237
+ const tmp6p2 = d1 - d6;
2238
+ const tmp2p2 = d2 + d5;
2239
+ const tmp5p2 = d2 - d5;
2240
+ const tmp3p2 = d3 + d4;
2241
+ const tmp4p2 = d3 - d4;
2242
+ let tmp10p2 = tmp0p2 + tmp3p2;
2243
+ const tmp13p2 = tmp0p2 - tmp3p2;
2244
+ let tmp11p2 = tmp1p2 + tmp2p2;
2245
+ let tmp12p2 = tmp1p2 - tmp2p2;
2246
+ data[dataOff] = tmp10p2 + tmp11p2;
2247
+ data[dataOff + 32] = tmp10p2 - tmp11p2;
2248
+ const z1p2 = (tmp12p2 + tmp13p2) * 0.707106781;
2249
+ data[dataOff + 16] = tmp13p2 + z1p2;
2250
+ data[dataOff + 48] = tmp13p2 - z1p2;
2251
+ tmp10p2 = tmp4p2 + tmp5p2;
2252
+ tmp11p2 = tmp5p2 + tmp6p2;
2253
+ tmp12p2 = tmp6p2 + tmp7p2;
2254
+ const z5p2 = (tmp10p2 - tmp12p2) * 0.382683433;
2255
+ const z2p2 = 0.5411961 * tmp10p2 + z5p2;
2256
+ const z4p2 = 1.306562965 * tmp12p2 + z5p2;
2257
+ const z3p2 = tmp11p2 * 0.707106781;
2258
+ const z11p2 = tmp7p2 + z3p2;
2259
+ const z13p2 = tmp7p2 - z3p2;
2260
+ data[dataOff + 40] = z13p2 + z2p2;
2261
+ data[dataOff + 24] = z13p2 - z2p2;
2262
+ data[dataOff + 8] = z11p2 + z4p2;
2263
+ data[dataOff + 56] = z11p2 - z4p2;
2264
+ dataOff++;
2265
+ }
2266
+ for (let i = 0; i < I64; ++i) {
2267
+ const fDCTVal = data[i] * fdtbl[i];
2268
+ this.outputfDCTQuant[i] = fDCTVal > 0 ? fDCTVal + 0.5 | 0 : fDCTVal - 0.5 | 0;
2269
+ }
2270
+ return this.outputfDCTQuant;
2271
+ }
2272
+ processDU(CDU, fdtbl, DC, HTDC, HTAC) {
2273
+ const EOB = HTAC[0];
2274
+ const M16zeroes = HTAC[240];
2275
+ let pos;
2276
+ const I16 = 16;
2277
+ const I63 = 63;
2278
+ const I64 = 64;
2279
+ const DU_DCT = this.fDCTQuant(CDU, fdtbl);
2280
+ for (let j = 0; j < I64; ++j) {
2281
+ this.DU[ZIGZAG[j]] = DU_DCT[j];
2282
+ }
2283
+ const Diff = this.DU[0] - DC;
2284
+ DC = this.DU[0];
2285
+ if (Diff === 0) {
2286
+ this.writeBits(HTDC[0]);
2287
+ } else {
2288
+ pos = 32767 + Diff;
2289
+ this.writeBits(HTDC[this.category[pos]]);
2290
+ this.writeBits(this.bitcode[pos]);
2291
+ }
2292
+ let end0pos = 63;
2293
+ for (; end0pos > 0 && this.DU[end0pos] === 0; end0pos--) {
2294
+ }
2295
+ if (end0pos === 0) {
2296
+ this.writeBits(EOB);
2297
+ return DC;
2298
+ }
2299
+ let i = 1;
2300
+ let lng;
2301
+ while (i <= end0pos) {
2302
+ const startpos = i;
2303
+ for (; this.DU[i] === 0 && i <= end0pos; ++i) {
2304
+ }
2305
+ let nrzeroes = i - startpos;
2306
+ if (nrzeroes >= I16) {
2307
+ lng = nrzeroes >> 4;
2308
+ for (let nrmarker = 1; nrmarker <= lng; ++nrmarker)
2309
+ this.writeBits(M16zeroes);
2310
+ nrzeroes = nrzeroes & 15;
2311
+ }
2312
+ pos = 32767 + this.DU[i];
2313
+ this.writeBits(HTAC[(nrzeroes << 4) + this.category[pos]]);
2314
+ this.writeBits(this.bitcode[pos]);
2315
+ i++;
2316
+ }
2317
+ if (end0pos !== I63) {
2318
+ this.writeBits(EOB);
2319
+ }
2320
+ return DC;
2321
+ }
2322
+ encode(rgba, width, height) {
2323
+ this.byteout = [];
2324
+ this.bytenew = 0;
2325
+ this.bytepos = 7;
2326
+ this.writeWord(65496);
2327
+ this.writeWord(65504);
2328
+ this.writeWord(16);
2329
+ this.writeByte(74);
2330
+ this.writeByte(70);
2331
+ this.writeByte(73);
2332
+ this.writeByte(70);
2333
+ this.writeByte(0);
2334
+ this.writeByte(1);
2335
+ this.writeByte(1);
2336
+ this.writeByte(0);
2337
+ this.writeWord(1);
2338
+ this.writeWord(1);
2339
+ this.writeByte(0);
2340
+ this.writeByte(0);
2341
+ this.writeWord(65499);
2342
+ this.writeWord(132);
2343
+ this.writeByte(0);
2344
+ for (let i = 0; i < 64; i++)
2345
+ this.writeByte(this.YTable[i]);
2346
+ this.writeByte(1);
2347
+ for (let j = 0; j < 64; j++)
2348
+ this.writeByte(this.UVTable[j]);
2349
+ this.writeWord(65472);
2350
+ this.writeWord(17);
2351
+ this.writeByte(8);
2352
+ this.writeWord(height);
2353
+ this.writeWord(width);
2354
+ this.writeByte(3);
2355
+ this.writeByte(1);
2356
+ this.writeByte(17);
2357
+ this.writeByte(0);
2358
+ this.writeByte(2);
2359
+ this.writeByte(17);
2360
+ this.writeByte(1);
2361
+ this.writeByte(3);
2362
+ this.writeByte(17);
2363
+ this.writeByte(1);
2364
+ this.writeWord(65476);
2365
+ this.writeWord(418);
2366
+ this.writeByte(0);
2367
+ for (let i = 0; i < 16; i++)
2368
+ this.writeByte(STD_DC_LUMINANCE_NRCODES[i + 1]);
2369
+ for (let i = 0; i <= 11; i++)
2370
+ this.writeByte(STD_DC_LUMINANCE_VALUES[i]);
2371
+ this.writeByte(16);
2372
+ for (let i = 0; i < 16; i++)
2373
+ this.writeByte(STD_AC_LUMINANCE_NRCODES[i + 1]);
2374
+ for (let i = 0; i <= 161; i++)
2375
+ this.writeByte(STD_AC_LUMINANCE_VALUES[i]);
2376
+ this.writeByte(1);
2377
+ for (let i = 0; i < 16; i++)
2378
+ this.writeByte(STD_DC_CHROMINANCE_NRCODES[i + 1]);
2379
+ for (let i = 0; i <= 11; i++)
2380
+ this.writeByte(STD_DC_CHROMINANCE_VALUES[i]);
2381
+ this.writeByte(17);
2382
+ for (let i = 0; i < 16; i++)
2383
+ this.writeByte(STD_AC_CHROMINANCE_NRCODES[i + 1]);
2384
+ for (let i = 0; i <= 161; i++)
2385
+ this.writeByte(STD_AC_CHROMINANCE_VALUES[i]);
2386
+ this.writeWord(65498);
2387
+ this.writeWord(12);
2388
+ this.writeByte(3);
2389
+ this.writeByte(1);
2390
+ this.writeByte(0);
2391
+ this.writeByte(2);
2392
+ this.writeByte(17);
2393
+ this.writeByte(3);
2394
+ this.writeByte(17);
2395
+ this.writeByte(0);
2396
+ this.writeByte(63);
2397
+ this.writeByte(0);
2398
+ let DCY = 0;
2399
+ let DCU = 0;
2400
+ let DCV = 0;
2401
+ this.bytenew = 0;
2402
+ this.bytepos = 7;
2403
+ const YDU = new Float32Array(64);
2404
+ const UDU = new Float32Array(64);
2405
+ const VDU = new Float32Array(64);
2406
+ const quadWidth = width * 4;
2407
+ const fdtbl_Y = this.fdtbl_Y;
2408
+ const fdtbl_UV = this.fdtbl_UV;
2409
+ const RGB_YUV_TABLE = this.RGB_YUV_TABLE;
2410
+ for (let y = 0; y < height; y += 8) {
2411
+ for (let x = 0; x < width; x += 8) {
2412
+ let start = quadWidth * y + x * 4;
2413
+ for (let pos = 0; pos < 64; pos++) {
2414
+ const row = pos >> 3;
2415
+ const col = (pos & 7) * 4;
2416
+ let p = start + row * quadWidth + col;
2417
+ if (y + row >= height)
2418
+ p -= quadWidth * (y + row - height + 1);
2419
+ if (x + (col >> 2) >= width)
2420
+ p -= 4 * (x + (col >> 2) - width + 1);
2421
+ const r = rgba[p];
2422
+ const g = rgba[p + 1];
2423
+ const b = rgba[p + 2];
2424
+ YDU[pos] = (RGB_YUV_TABLE[r] + RGB_YUV_TABLE[g + 256] + RGB_YUV_TABLE[b + 512] >> 16) - 128;
2425
+ UDU[pos] = (RGB_YUV_TABLE[r + 768] + RGB_YUV_TABLE[g + 1024] + RGB_YUV_TABLE[b + 1280] >> 16) - 128;
2426
+ VDU[pos] = (RGB_YUV_TABLE[r + 1280] + RGB_YUV_TABLE[g + 1536] + RGB_YUV_TABLE[b + 1792] >> 16) - 128;
2427
+ }
2428
+ DCY = this.processDU(YDU, fdtbl_Y, DCY, this.YDC_HT, this.YAC_HT);
2429
+ DCU = this.processDU(UDU, fdtbl_UV, DCU, this.UVDC_HT, this.UVAC_HT);
2430
+ DCV = this.processDU(VDU, fdtbl_UV, DCV, this.UVDC_HT, this.UVAC_HT);
2431
+ }
2432
+ }
2433
+ if (this.bytepos >= 0) {
2434
+ const fillbits = { length: this.bytepos + 1, code: (1 << this.bytepos + 1) - 1 };
2435
+ this.writeBits(fillbits);
2436
+ }
2437
+ this.writeWord(65497);
2438
+ return Buffer.from(this.byteout);
2439
+ }
2440
+ };
2441
+ }
2442
+ });
2443
+
1471
2444
  // ../core/dist/png-encoder.js
1472
2445
  import { deflateSync } from "zlib";
1473
2446
  function crc32(buf) {
@@ -1531,12 +2504,18 @@ var init_png_encoder = __esm({
1531
2504
  import * as fs from "fs";
1532
2505
  import * as os from "os";
1533
2506
  import * as path from "path";
1534
- function encodePngFromRgbaResponse(response) {
2507
+ function encodeImageFromRgbaResponse(response, format, quality) {
1535
2508
  if (!response.data || response.width === void 0 || response.height === void 0) {
1536
2509
  throw new Error("Render response missing data, width, or height");
1537
2510
  }
1538
2511
  const rgbaBuffer = Buffer.from(response.data, "base64");
1539
- return rgbaToPng(rgbaBuffer, response.width, response.height);
2512
+ if (format === "png") {
2513
+ return { buffer: rgbaToPng(rgbaBuffer, response.width, response.height), mimeType: "image/png" };
2514
+ }
2515
+ return {
2516
+ buffer: rgbaToJpeg(rgbaBuffer, response.width, response.height, quality),
2517
+ mimeType: "image/jpeg"
2518
+ };
1540
2519
  }
1541
2520
  function luaLongQuote(s) {
1542
2521
  let level = 0;
@@ -1751,6 +2730,9 @@ function parseBridgeResponse(response) {
1751
2730
  }
1752
2731
  return JSON.stringify(response);
1753
2732
  }
2733
+ function sleep(ms) {
2734
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2735
+ }
1754
2736
  var SERVER_LOCAL_NAME, CLIENT_LOCAL_NAME, EVAL_WRAPPER_LINE_OFFSET, RobloxStudioTools;
1755
2737
  var init_tools = __esm({
1756
2738
  "../core/dist/tools/index.js"() {
@@ -1760,6 +2742,7 @@ var init_tools = __esm({
1760
2742
  init_build_executor();
1761
2743
  init_opencloud_client();
1762
2744
  init_roblox_cookie_client();
2745
+ init_jpeg_encoder();
1763
2746
  init_png_encoder();
1764
2747
  SERVER_LOCAL_NAME = "__MCP_ServerEvalLocal";
1765
2748
  CLIENT_LOCAL_NAME = "__MCP_ClientEvalBridge";
@@ -1796,6 +2779,111 @@ var init_tools = __esm({
1796
2779
  }
1797
2780
  return this.client.request(endpoint, data, r.targetInstanceId, r.targetRole);
1798
2781
  }
2782
+ // Resolves which connected place a tool should target and whether a playtest
2783
+ // CLIENT peer is present on it. Used by capture/input to auto-route to the
2784
+ // running client (where the live viewport + input pipeline are) without the
2785
+ // caller having to pass target. Throws RoutingFailure with the standard
2786
+ // instance list if the place is ambiguous (multiple connected, no instance_id).
2787
+ _resolveRuntime(instance_id) {
2788
+ const r = this.bridge.resolveTarget({ instance_id, target: void 0 });
2789
+ if (!r.ok)
2790
+ throw new RoutingFailure(r.error);
2791
+ const resolvedId = r.targetInstanceId;
2792
+ const roles = this.bridge.getInstances().filter((i) => i.instanceId === resolvedId).map((i) => i.role);
2793
+ const clientRoles = roles.filter((role) => role.startsWith("client")).sort();
2794
+ return { instanceId: resolvedId, clientRole: clientRoles[0] };
2795
+ }
2796
+ _resolveInstanceIdOnly(instance_id) {
2797
+ const instances = this.bridge.getInstances();
2798
+ const publicList = this.bridge.getPublicInstances();
2799
+ const errorData = { instances: publicList, count: publicList.length };
2800
+ if (instance_id !== void 0) {
2801
+ if (!instances.some((i) => i.instanceId === instance_id)) {
2802
+ throw new RoutingFailure({
2803
+ code: "unrecognized_instance_id",
2804
+ message: `instance_id "${instance_id}" is not connected. Pass one from data.instances.`,
2805
+ data: errorData
2806
+ });
2807
+ }
2808
+ return instance_id;
2809
+ }
2810
+ const distinct = Array.from(new Set(instances.map((i) => i.instanceId)));
2811
+ if (distinct.length === 0) {
2812
+ throw new RoutingFailure({
2813
+ code: "unrecognized_instance_id",
2814
+ message: "No Studio plugin is connected.",
2815
+ data: errorData
2816
+ });
2817
+ }
2818
+ if (distinct.length > 1) {
2819
+ throw new RoutingFailure({
2820
+ code: "multiple_instances_connected",
2821
+ message: "Multiple Studio places are connected. Pass instance_id to disambiguate.",
2822
+ data: errorData
2823
+ });
2824
+ }
2825
+ return distinct[0];
2826
+ }
2827
+ _resolveSingleTarget(target, instance_id) {
2828
+ const resolved = this.bridge.resolveTarget({ instance_id, target });
2829
+ if (!resolved.ok)
2830
+ throw new RoutingFailure(resolved.error);
2831
+ if (resolved.mode !== "single") {
2832
+ throw new RoutingFailure({
2833
+ code: "target_role_not_present_on_instance",
2834
+ message: "Pick a specific target role for this tool.",
2835
+ data: {
2836
+ instances: this.bridge.getPublicInstances(),
2837
+ count: this.bridge.getInstances().length
2838
+ }
2839
+ });
2840
+ }
2841
+ return { instanceId: resolved.targetInstanceId, role: resolved.targetRole };
2842
+ }
2843
+ _rolesForInstance(instanceId) {
2844
+ return this.bridge.getInstances().filter((i) => i.instanceId === instanceId).map((i) => i.role);
2845
+ }
2846
+ _clientRolesForInstance(instanceId) {
2847
+ return this._rolesForInstance(instanceId).filter((role) => /^client-\d+$/.test(role)).sort((a, b) => Number(a.slice("client-".length)) - Number(b.slice("client-".length)));
2848
+ }
2849
+ async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30) {
2850
+ const deadline = Date.now() + timeoutSec * 1e3;
2851
+ while (Date.now() < deadline) {
2852
+ const roles = this._rolesForInstance(instanceId);
2853
+ const hasServer = !opts.server || roles.includes("server");
2854
+ const hasClients = opts.clientCount === void 0 || this._clientRolesForInstance(instanceId).length >= opts.clientCount;
2855
+ const absent = opts.absentRole === void 0 || !roles.includes(opts.absentRole);
2856
+ const runtimeAbsent = !opts.noRuntime || !roles.some((role) => role === "server" || /^client-\d+$/.test(role));
2857
+ if (hasServer && hasClients && absent && runtimeAbsent) {
2858
+ return { ok: true, roles, timedOut: false };
2859
+ }
2860
+ await sleep(250);
2861
+ }
2862
+ return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
2863
+ }
2864
+ async _waitForExactClientCount(instanceId, expectedClientCount, timeoutSec = 30, stableMs = 3e3) {
2865
+ const deadline = Date.now() + timeoutSec * 1e3;
2866
+ let exactSince;
2867
+ while (Date.now() < deadline) {
2868
+ const roles2 = this._rolesForInstance(instanceId);
2869
+ const clientCount2 = this._clientRolesForInstance(instanceId).length;
2870
+ if (clientCount2 > expectedClientCount) {
2871
+ return { ok: false, roles: roles2, timedOut: false, extraClients: true, clientCount: clientCount2 };
2872
+ }
2873
+ if (roles2.includes("server") && clientCount2 === expectedClientCount) {
2874
+ exactSince ??= Date.now();
2875
+ if (Date.now() - exactSince >= stableMs) {
2876
+ return { ok: true, roles: roles2, timedOut: false, extraClients: false, clientCount: clientCount2 };
2877
+ }
2878
+ } else {
2879
+ exactSince = void 0;
2880
+ }
2881
+ await sleep(250);
2882
+ }
2883
+ const roles = this._rolesForInstance(instanceId);
2884
+ const clientCount = this._clientRolesForInstance(instanceId).length;
2885
+ return { ok: false, roles, timedOut: true, extraClients: clientCount > expectedClientCount, clientCount };
2886
+ }
1799
2887
  async getFileTree(path2 = "", instance_id) {
1800
2888
  const response = await this._callSingle("/api/file-tree", { path: path2 }, void 0, instance_id);
1801
2889
  return {
@@ -2325,7 +3413,7 @@ ${code}`
2325
3413
  const wrapper = buildModuleScriptInvokeWrapper({
2326
3414
  service: "ServerScriptService",
2327
3415
  bridgeName: SERVER_LOCAL_NAME,
2328
- missingError: "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
3416
+ missingError: "ServerEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically (including for manually-started playtests); if a playtest is running and you still see this, reconnect the plugin in the edit window so the bridge reinstalls, then start the playtest again.",
2329
3417
  userCode: code
2330
3418
  });
2331
3419
  const response = await this._callSingle("/api/execute-luau", { code: wrapper }, "server", instance_id);
@@ -2349,7 +3437,7 @@ ${code}`
2349
3437
  const wrapper = buildModuleScriptInvokeWrapper({
2350
3438
  service: "ReplicatedStorage",
2351
3439
  bridgeName: CLIENT_LOCAL_NAME,
2352
- missingError: "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
3440
+ missingError: "ClientEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically (including for manually-started playtests); if a playtest is running and you still see this, reconnect the plugin in the edit window so the bridge reinstalls, then start the playtest again.",
2353
3441
  userCode: code
2354
3442
  });
2355
3443
  const response = await this._callSingle("/api/execute-luau", { code: wrapper }, clientTarget, instance_id);
@@ -2440,10 +3528,10 @@ ${code}`
2440
3528
  if (mode !== "play" && mode !== "run") {
2441
3529
  throw new Error('mode must be "play" or "run"');
2442
3530
  }
2443
- const data = { mode };
2444
3531
  if (numPlayers !== void 0) {
2445
- data.numPlayers = numPlayers;
3532
+ throw new Error("start_playtest is single-player only. Use multiplayer_test_start for multi-client StudioTestService sessions.");
2446
3533
  }
3534
+ const data = { mode };
2447
3535
  const response = await this._callSingle("/api/start-playtest", data, void 0, instance_id);
2448
3536
  return {
2449
3537
  content: [
@@ -2471,6 +3559,196 @@ ${code}`
2471
3559
  ]
2472
3560
  };
2473
3561
  }
3562
+ async _buildMultiplayerState(instanceId) {
3563
+ const peers = this.bridge.getPublicInstances().filter((i) => i.instanceId === instanceId).sort((a, b) => a.role.localeCompare(b.role));
3564
+ const body = {
3565
+ instanceId,
3566
+ peers,
3567
+ peerCount: peers.length
3568
+ };
3569
+ const edit = peers.find((p) => p.role === "edit");
3570
+ const server = peers.find((p) => p.role === "server");
3571
+ let editState;
3572
+ let serverState;
3573
+ if (edit) {
3574
+ try {
3575
+ editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
3576
+ body.edit = editState;
3577
+ } catch (err) {
3578
+ body.edit = { error: err instanceof Error ? err.message : String(err) };
3579
+ }
3580
+ }
3581
+ if (server) {
3582
+ try {
3583
+ serverState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "server");
3584
+ body.server = serverState;
3585
+ } catch (err) {
3586
+ body.server = { error: err instanceof Error ? err.message : String(err) };
3587
+ }
3588
+ }
3589
+ const session = editState?.session;
3590
+ const rawPhase = typeof session?.phase === "string" ? session.phase : void 0;
3591
+ const hasRuntime = peers.some((p) => p.role === "server" || p.role.startsWith("client-"));
3592
+ body.phase = rawPhase === "starting" && hasRuntime ? "running" : rawPhase ?? (hasRuntime ? "running" : "idle");
3593
+ body.testId = session?.testId;
3594
+ body.numPlayers = session?.numPlayers;
3595
+ body.testArgs = session?.testArgs ?? serverState?.testArgs;
3596
+ body.result = session?.result;
3597
+ body.error = session?.error;
3598
+ body.players = serverState?.players ?? [];
3599
+ body.playerCount = serverState?.playerCount ?? 0;
3600
+ body.clientRoles = this._clientRolesForInstance(instanceId);
3601
+ return body;
3602
+ }
3603
+ async _waitForMultiplayerEditDone(instanceId, timeoutSec = 30) {
3604
+ const deadline = Date.now() + timeoutSec * 1e3;
3605
+ while (Date.now() < deadline) {
3606
+ if (!this._rolesForInstance(instanceId).includes("edit"))
3607
+ return false;
3608
+ try {
3609
+ const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
3610
+ const phase = editState?.session?.phase;
3611
+ if (phase === "completed" || phase === "failed")
3612
+ return true;
3613
+ } catch {
3614
+ }
3615
+ await sleep(250);
3616
+ }
3617
+ return false;
3618
+ }
3619
+ async _isMultiplayerTestRunning(instanceId) {
3620
+ if (!this._rolesForInstance(instanceId).includes("edit"))
3621
+ return false;
3622
+ try {
3623
+ const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
3624
+ const phase = editState?.session?.phase;
3625
+ return phase === "starting" || phase === "running";
3626
+ } catch {
3627
+ return false;
3628
+ }
3629
+ }
3630
+ async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30) {
3631
+ const deadline = Date.now() + timeoutSec * 1e3;
3632
+ while (Date.now() < deadline) {
3633
+ const exact = await this._waitForExactClientCount(instanceId, clientCount, 0.25, 0);
3634
+ if (exact.ok || exact.extraClients) {
3635
+ return { ok: exact.ok, roles: exact.roles, timedOut: false, error: exact.extraClients ? `Expected ${clientCount} client(s), but Studio registered ${exact.clientCount}.` : void 0 };
3636
+ }
3637
+ try {
3638
+ const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
3639
+ const session = editState?.session;
3640
+ if (session?.phase === "failed" || session?.phase === "completed") {
3641
+ return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: false, phase: session.phase, error: session.error };
3642
+ }
3643
+ } catch {
3644
+ }
3645
+ await sleep(250);
3646
+ }
3647
+ return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
3648
+ }
3649
+ async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id) {
3650
+ if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
3651
+ throw new Error("numPlayers must be an integer from 1 to 8");
3652
+ }
3653
+ const editTarget = this._resolveSingleTarget("edit", instance_id);
3654
+ const response = await this.client.request("/api/multiplayer-test-start", { numPlayers, testArgs: testArgs ?? {} }, editTarget.instanceId, editTarget.role);
3655
+ if (response?.error) {
3656
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
3657
+ }
3658
+ const wait = await this._waitForMultiplayerStart(editTarget.instanceId, numPlayers, timeout ?? 30);
3659
+ const state = await this._buildMultiplayerState(editTarget.instanceId);
3660
+ return {
3661
+ content: [{
3662
+ type: "text",
3663
+ text: JSON.stringify({
3664
+ ...response,
3665
+ ready: wait.ok,
3666
+ timedOut: wait.timedOut,
3667
+ wait,
3668
+ roles: wait.roles,
3669
+ state
3670
+ })
3671
+ }]
3672
+ };
3673
+ }
3674
+ async multiplayerTestState(instance_id) {
3675
+ const instanceId = this._resolveInstanceIdOnly(instance_id);
3676
+ const state = await this._buildMultiplayerState(instanceId);
3677
+ return { content: [{ type: "text", text: JSON.stringify(state) }] };
3678
+ }
3679
+ async multiplayerTestAddPlayers(numPlayers, timeout, instance_id) {
3680
+ if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
3681
+ throw new Error("numPlayers must be an integer from 1 to 8");
3682
+ }
3683
+ const serverTarget = this._resolveSingleTarget("server", instance_id);
3684
+ const before = this._clientRolesForInstance(serverTarget.instanceId).length;
3685
+ const response = await this.client.request("/api/multiplayer-test-add-players", { numPlayers, timeout: timeout ?? 10 }, serverTarget.instanceId, serverTarget.role);
3686
+ if (response?.error) {
3687
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
3688
+ }
3689
+ const wait = await this._waitForExactClientCount(serverTarget.instanceId, before + numPlayers, timeout ?? 30);
3690
+ const state = await this._buildMultiplayerState(serverTarget.instanceId);
3691
+ return {
3692
+ content: [{
3693
+ type: "text",
3694
+ text: JSON.stringify({
3695
+ ...response,
3696
+ ready: wait.ok,
3697
+ timedOut: wait.timedOut,
3698
+ wait,
3699
+ roles: wait.roles,
3700
+ state
3701
+ })
3702
+ }]
3703
+ };
3704
+ }
3705
+ async multiplayerTestLeaveClient(target = "client-1", timeout, instance_id) {
3706
+ if (!/^client-\d+$/.test(target)) {
3707
+ throw new Error(`multiplayer_test_leave_client requires target=client-N (got: ${target})`);
3708
+ }
3709
+ const clientTarget = this._resolveSingleTarget(target, instance_id);
3710
+ const response = await this.client.request("/api/multiplayer-test-leave-client", {}, clientTarget.instanceId, clientTarget.role);
3711
+ if (response?.error) {
3712
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
3713
+ }
3714
+ const wait = await this._waitForRuntimeRoles(clientTarget.instanceId, { absentRole: clientTarget.role }, timeout ?? 30);
3715
+ const state = await this._buildMultiplayerState(clientTarget.instanceId);
3716
+ return {
3717
+ content: [{
3718
+ type: "text",
3719
+ text: JSON.stringify({
3720
+ ...response,
3721
+ left: wait.ok,
3722
+ timedOut: wait.timedOut,
3723
+ roles: wait.roles,
3724
+ state
3725
+ })
3726
+ }]
3727
+ };
3728
+ }
3729
+ async multiplayerTestEnd(value, timeout, instance_id) {
3730
+ const serverTarget = this._resolveSingleTarget("server", instance_id);
3731
+ const response = await this.client.request("/api/multiplayer-test-end", { value: value ?? "ended_by_mcp" }, serverTarget.instanceId, serverTarget.role);
3732
+ if (response?.error) {
3733
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
3734
+ }
3735
+ const editDone = await this._waitForMultiplayerEditDone(serverTarget.instanceId, timeout ?? 30);
3736
+ const wait = await this._waitForRuntimeRoles(serverTarget.instanceId, { noRuntime: true }, timeout ?? 30);
3737
+ const state = await this._buildMultiplayerState(serverTarget.instanceId);
3738
+ return {
3739
+ content: [{
3740
+ type: "text",
3741
+ text: JSON.stringify({
3742
+ ...response,
3743
+ ended: wait.ok,
3744
+ editDone,
3745
+ timedOut: wait.timedOut,
3746
+ roles: wait.roles,
3747
+ state
3748
+ })
3749
+ }]
3750
+ };
3751
+ }
2474
3752
  async getConnectedInstances() {
2475
3753
  const instances = this.bridge.getPublicInstances();
2476
3754
  return {
@@ -2506,15 +3784,15 @@ ${code}`
2506
3784
  }
2507
3785
  static findProjectRoot(startDir) {
2508
3786
  let dir = path.resolve(startDir);
2509
- while (true) {
3787
+ let previous = "";
3788
+ while (dir !== previous) {
2510
3789
  if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "package.json"))) {
2511
3790
  return dir;
2512
3791
  }
2513
- const parent = path.dirname(dir);
2514
- if (parent === dir)
2515
- return null;
2516
- dir = parent;
3792
+ previous = dir;
3793
+ dir = path.dirname(dir);
2517
3794
  }
3795
+ return null;
2518
3796
  }
2519
3797
  static isDirectory(candidate) {
2520
3798
  if (!candidate)
@@ -3195,13 +4473,13 @@ ${code}`
3195
4473
  if (!action) {
3196
4474
  throw new Error("action is required for simulate_mouse_input");
3197
4475
  }
4476
+ const { instanceId, clientRole } = this._resolveRuntime(instance_id);
3198
4477
  const response = await this._callSingle("/api/simulate-mouse-input", {
3199
4478
  action,
3200
4479
  x,
3201
4480
  y,
3202
- button,
3203
- scrollDirection
3204
- }, target || "edit", instance_id);
4481
+ button
4482
+ }, target || clientRole || "edit", instanceId);
3205
4483
  return {
3206
4484
  content: [{
3207
4485
  type: "text",
@@ -3209,15 +4487,17 @@ ${code}`
3209
4487
  }]
3210
4488
  };
3211
4489
  }
3212
- async simulateKeyboardInput(keyCode, action, duration, target, instance_id) {
3213
- if (!keyCode) {
3214
- throw new Error("keyCode is required for simulate_keyboard_input");
4490
+ async simulateKeyboardInput(keyCode, action, duration, text, target, instance_id) {
4491
+ if (!keyCode && text === void 0) {
4492
+ throw new Error("keyCode or text is required for simulate_keyboard_input");
3215
4493
  }
4494
+ const { instanceId, clientRole } = this._resolveRuntime(instance_id);
3216
4495
  const response = await this._callSingle("/api/simulate-keyboard-input", {
3217
4496
  keyCode,
3218
4497
  action,
3219
- duration
3220
- }, target || "edit", instance_id);
4498
+ duration,
4499
+ text
4500
+ }, target || clientRole || "edit", instanceId);
3221
4501
  return {
3222
4502
  content: [{
3223
4503
  type: "text",
@@ -3429,23 +4709,74 @@ ${code}`
3429
4709
  }, tgt, instance_id);
3430
4710
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
3431
4711
  }
3432
- async captureScreenshot(instance_id) {
3433
- const response = await this._callSingle("/api/capture-screenshot", {}, void 0, instance_id);
4712
+ async captureScreenshot(instance_id, format, quality) {
4713
+ const { instanceId, clientRole } = this._resolveRuntime(instance_id);
4714
+ let response;
4715
+ if (clientRole) {
4716
+ const begin = await this._callSingle("/api/capture-begin", {}, clientRole, instanceId);
4717
+ if (begin.error) {
4718
+ return { content: [{ type: "text", text: begin.error }] };
4719
+ }
4720
+ if (!begin.contentId) {
4721
+ return { content: [{ type: "text", text: "Screenshot capture failed: no content id returned from client." }] };
4722
+ }
4723
+ response = await this._callSingle("/api/capture-read", { contentId: begin.contentId }, "edit", instanceId);
4724
+ } else {
4725
+ response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
4726
+ }
3434
4727
  if (response.error) {
4728
+ let text = response.error;
4729
+ if (clientRole && response.error.includes("Failed to load texture, unexpected format") && await this._isMultiplayerTestRunning(instanceId)) {
4730
+ text = `Screenshot capture reached the multiplayer client, but Roblox returned a temporary screenshot texture that the edit peer cannot read in StudioTestService multiplayer sessions. Regular start_playtest capture works because the temporary rbxtemp:// handle is readable from the edit process; multiplayer client handles appear to be scoped to the client process. Raw error: ${response.error}`;
4731
+ }
3435
4732
  return {
3436
4733
  content: [{
3437
4734
  type: "text",
3438
- text: response.error
4735
+ text
3439
4736
  }]
3440
4737
  };
3441
4738
  }
3442
- const pngBuffer = encodePngFromRgbaResponse(response);
4739
+ const w = response.width;
4740
+ const h = response.height;
4741
+ if (w === void 0 || h === void 0) {
4742
+ return { content: [{ type: "text", text: "Screenshot response missing dimensions." }] };
4743
+ }
4744
+ const fmt = format === "png" ? "png" : "jpeg";
4745
+ const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
4746
+ const MAX_IMAGE_BYTES = 6e6;
4747
+ const encoded = encodeImageFromRgbaResponse(response, fmt, q);
4748
+ let { buffer } = encoded;
4749
+ const { mimeType } = encoded;
4750
+ let usedQ = q;
4751
+ let note = "";
4752
+ if (buffer.length > MAX_IMAGE_BYTES) {
4753
+ if (fmt === "png") {
4754
+ const mb = (buffer.length / 1048576).toFixed(1);
4755
+ return {
4756
+ content: [{
4757
+ type: "text",
4758
+ text: `PNG screenshot is ${mb}MB, over the ~${(MAX_IMAGE_BYTES / 1048576).toFixed(0)}MB inline image limit. Use the default jpeg format (optionally with a "quality" value) or make the Studio window smaller for a lossless capture.`
4759
+ }]
4760
+ };
4761
+ }
4762
+ while (buffer.length > MAX_IMAGE_BYTES && usedQ > 25) {
4763
+ usedQ = Math.max(25, usedQ - 20);
4764
+ buffer = encodeImageFromRgbaResponse(response, "jpeg", usedQ).buffer;
4765
+ }
4766
+ note = ` \u2014 auto-reduced to q${usedQ} to fit the inline size limit; enlarge the Studio window or capture a smaller region for finer detail`;
4767
+ }
3443
4768
  return {
3444
- content: [{
3445
- type: "image",
3446
- data: pngBuffer.toString("base64"),
3447
- mimeType: "image/png"
3448
- }]
4769
+ content: [
4770
+ {
4771
+ type: "text",
4772
+ text: `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image.`
4773
+ },
4774
+ {
4775
+ type: "image",
4776
+ data: buffer.toString("base64"),
4777
+ mimeType
4778
+ }
4779
+ ]
3449
4780
  };
3450
4781
  }
3451
4782
  };
@@ -4582,7 +5913,7 @@ var init_definitions = __esm({
4582
5913
  {
4583
5914
  name: "execute_luau",
4584
5915
  category: "write",
4585
- description: "Execute Luau code in plugin context. Use print()/warn() for output. Return value is captured.",
5916
+ description: 'Execute Luau code in plugin context. target="server" and target="client-N" run against live runtime DataModels with PluginSecurity permissions; use eval_*_runtime instead when you need the game Script/LocalScript VM require cache. Use print()/warn() for output. Return value is captured.',
4586
5917
  inputSchema: {
4587
5918
  type: "object",
4588
5919
  properties: {
@@ -4605,7 +5936,7 @@ var init_definitions = __esm({
4605
5936
  {
4606
5937
  name: "eval_server_runtime",
4607
5938
  category: "write",
4608
- description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts). Use this instead of execute_luau target=server when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest.",
5939
+ description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts, unlike execute_luau target=server which runs in plugin context). Requires a running playtest; the bridge is installed automatically (including for playtests started manually via the Studio Play button).",
4609
5940
  inputSchema: {
4610
5941
  type: "object",
4611
5942
  properties: {
@@ -4624,7 +5955,7 @@ var init_definitions = __esm({
4624
5955
  {
4625
5956
  name: "eval_client_runtime",
4626
5957
  category: "write",
4627
- description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts). Use this instead of execute_luau target=client-N when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest.",
5958
+ description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts, unlike execute_luau target=client-N which runs in plugin context). Requires a running playtest; the bridge is installed automatically (including for playtests started manually via the Studio Play button).",
4628
5959
  inputSchema: {
4629
5960
  type: "object",
4630
5961
  properties: {
@@ -4701,7 +6032,7 @@ var init_definitions = __esm({
4701
6032
  {
4702
6033
  name: "start_playtest",
4703
6034
  category: "write",
4704
- description: "Start playtest. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest. Use numPlayers for multi-client testing (server + N clients).",
6035
+ description: "Start a simple single-player Studio playtest in play or run mode. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest. For multi-client testing use multiplayer_test_start instead.",
4705
6036
  inputSchema: {
4706
6037
  type: "object",
4707
6038
  properties: {
@@ -4712,7 +6043,7 @@ var init_definitions = __esm({
4712
6043
  },
4713
6044
  numPlayers: {
4714
6045
  type: "number",
4715
- description: "Number of client players (1-8). Triggers server + clients mode via TestService."
6046
+ description: "Deprecated and rejected. Use multiplayer_test_start for multi-client testing."
4716
6047
  },
4717
6048
  instance_id: {
4718
6049
  type: "string",
@@ -4754,6 +6085,112 @@ var init_definitions = __esm({
4754
6085
  }
4755
6086
  }
4756
6087
  },
6088
+ {
6089
+ name: "multiplayer_test_start",
6090
+ category: "write",
6091
+ description: "Start a StudioTestService multiplayer test and wait for the server plus requested client peers to connect. Use this for multi-client runtime testing.",
6092
+ inputSchema: {
6093
+ type: "object",
6094
+ properties: {
6095
+ numPlayers: {
6096
+ type: "number",
6097
+ description: "Number of client players to start (1-8)."
6098
+ },
6099
+ testArgs: {
6100
+ description: "JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients."
6101
+ },
6102
+ timeout: {
6103
+ type: "number",
6104
+ description: "Max seconds to wait for server + clients to register (default 30)."
6105
+ },
6106
+ instance_id: {
6107
+ type: "string",
6108
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
6109
+ }
6110
+ },
6111
+ required: ["numPlayers"]
6112
+ }
6113
+ },
6114
+ {
6115
+ name: "multiplayer_test_state",
6116
+ category: "read",
6117
+ description: "Get the active multiplayer StudioTestService state for a place: phase, peers, players, original testArgs, result/error, and connected client roles.",
6118
+ inputSchema: {
6119
+ type: "object",
6120
+ properties: {
6121
+ instance_id: {
6122
+ type: "string",
6123
+ description: "Which connected Studio place to inspect. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
6124
+ }
6125
+ }
6126
+ }
6127
+ },
6128
+ {
6129
+ name: "multiplayer_test_add_players",
6130
+ category: "write",
6131
+ description: "Add client players to a running StudioTestService multiplayer test and wait for the new clients to connect.",
6132
+ inputSchema: {
6133
+ type: "object",
6134
+ properties: {
6135
+ numPlayers: {
6136
+ type: "number",
6137
+ description: "Number of additional client players to add (1-8)."
6138
+ },
6139
+ timeout: {
6140
+ type: "number",
6141
+ description: "Max seconds to wait for new clients to register (default 30)."
6142
+ },
6143
+ instance_id: {
6144
+ type: "string",
6145
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
6146
+ }
6147
+ },
6148
+ required: ["numPlayers"]
6149
+ }
6150
+ },
6151
+ {
6152
+ name: "multiplayer_test_leave_client",
6153
+ category: "write",
6154
+ description: "Disconnect a specific client from a running StudioTestService multiplayer test, then wait for that client peer to leave.",
6155
+ inputSchema: {
6156
+ type: "object",
6157
+ properties: {
6158
+ target: {
6159
+ type: "string",
6160
+ description: 'Client target to leave: "client-1" (default), "client-2", etc.'
6161
+ },
6162
+ timeout: {
6163
+ type: "number",
6164
+ description: "Max seconds to wait for the client peer to disconnect (default 30)."
6165
+ },
6166
+ instance_id: {
6167
+ type: "string",
6168
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
6169
+ }
6170
+ }
6171
+ }
6172
+ },
6173
+ {
6174
+ name: "multiplayer_test_end",
6175
+ category: "write",
6176
+ description: "End a running StudioTestService multiplayer test with an optional return value, then wait for all runtime peers to disconnect.",
6177
+ inputSchema: {
6178
+ type: "object",
6179
+ properties: {
6180
+ value: {
6181
+ description: "JSON-compatible value returned to the edit-side ExecuteMultiplayerTestAsync call."
6182
+ },
6183
+ timeout: {
6184
+ type: "number",
6185
+ description: "Max seconds to wait for runtime peers to disconnect (default 30)."
6186
+ },
6187
+ instance_id: {
6188
+ type: "string",
6189
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
6190
+ }
6191
+ }
6192
+ }
6193
+ },
4757
6194
  {
4758
6195
  name: "get_runtime_logs",
4759
6196
  category: "read",
@@ -5298,10 +6735,19 @@ part(0,2,0,2,1,1,"b")`,
5298
6735
  {
5299
6736
  name: "capture_screenshot",
5300
6737
  category: "read",
5301
- description: 'Capture a screenshot of the Roblox Studio viewport and return it as a PNG image. Requires EditableImage API to be enabled: Game Settings > Security > "Allow Mesh / Image APIs". Only works in Edit mode with the viewport visible.',
6738
+ description: 'Capture the Roblox Studio viewport at native resolution and return it as an image, plus a text line stating the exact pixel dimensions. Works in Edit mode and regular playtests (auto-detects a running client and captures the live play viewport). StudioTestService multiplayer client screenshots are currently blocked by Roblox temporary-texture process scoping; the tool returns a clear error in that case. The returned image is never downscaled, so its pixel grid is exactly the coordinate space simulate_mouse_input uses \u2014 read click positions straight off this image. For reading fine text/UI, use format="png" (lossless) or a higher quality; enlarging the Studio window raises resolution. Requires EditableImage API enabled (Game Settings > Security > "Allow Mesh / Image APIs") and the window to be visible.',
5302
6739
  inputSchema: {
5303
6740
  type: "object",
5304
6741
  properties: {
6742
+ format: {
6743
+ type: "string",
6744
+ enum: ["jpeg", "png"],
6745
+ description: 'Image format. "jpeg" (default) is compact and crisp at high quality. "png" is lossless \u2014 best for reading dense text/UI, but larger (a busy 3D scene may be big).'
6746
+ },
6747
+ quality: {
6748
+ type: "number",
6749
+ description: "JPEG quality 1-100 (default 92). Higher = sharper text, larger size. Ignored for png."
6750
+ },
5305
6751
  instance_id: {
5306
6752
  type: "string",
5307
6753
  description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
@@ -5313,36 +6759,31 @@ part(0,2,0,2,1,1,"b")`,
5313
6759
  {
5314
6760
  name: "simulate_mouse_input",
5315
6761
  category: "write",
5316
- description: "Simulate mouse input in the Roblox Studio viewport via VirtualInputManager. Use during playtest to click UI buttons, interact with objects, or navigate menus. Coordinates are viewport pixels (top-left is 0,0). Use capture_screenshot to identify UI element positions before clicking.",
6762
+ description: "Simulate a mouse click in the running game via UserInputService:CreateVirtualInput. Use during a playtest to click UI buttons, interact with objects, or aim. Fires real UserInputService input and activates GUI buttons. Coordinates are viewport pixels matching capture_screenshot (top-left is 0,0) \u2014 take a screenshot first to find positions. Auto-targets the running client; only works during a playtest. Note: only click/mouseDown/mouseUp are supported (the API has no mouse-move or scroll).",
5317
6763
  inputSchema: {
5318
6764
  type: "object",
5319
6765
  properties: {
5320
6766
  action: {
5321
6767
  type: "string",
5322
- enum: ["click", "mouseDown", "mouseUp", "move", "scroll"],
5323
- description: 'Mouse action to perform. "click" does mouseDown + short delay + mouseUp.'
6768
+ enum: ["click", "mouseDown", "mouseUp"],
6769
+ description: 'Mouse action. "click" does mouseDown + short delay + mouseUp.'
5324
6770
  },
5325
6771
  x: {
5326
6772
  type: "number",
5327
- description: "Viewport pixel X coordinate"
6773
+ description: "Viewport pixel X coordinate (as seen in capture_screenshot)"
5328
6774
  },
5329
6775
  y: {
5330
6776
  type: "number",
5331
- description: "Viewport pixel Y coordinate"
6777
+ description: "Viewport pixel Y coordinate (as seen in capture_screenshot)"
5332
6778
  },
5333
6779
  button: {
5334
6780
  type: "string",
5335
6781
  enum: ["Left", "Right", "Middle"],
5336
6782
  description: "Mouse button (default: Left)"
5337
6783
  },
5338
- scrollDirection: {
5339
- type: "string",
5340
- enum: ["up", "down"],
5341
- description: 'Scroll direction (only for "scroll" action)'
5342
- },
5343
6784
  target: {
5344
6785
  type: "string",
5345
- description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
6786
+ description: 'Instance target. Defaults to the running playtest client (client-1) when present, else "edit". Override with "server", "client-2", etc.'
5346
6787
  },
5347
6788
  instance_id: {
5348
6789
  type: "string",
@@ -5355,13 +6796,13 @@ part(0,2,0,2,1,1,"b")`,
5355
6796
  {
5356
6797
  name: "simulate_keyboard_input",
5357
6798
  category: "write",
5358
- description: 'Simulate keyboard input via VirtualInputManager. Use during playtest for character movement (W/A/S/D), jumping (Space), interactions (E), or any key-driven action. For sustained movement, use "press" to hold and "release" to let go.',
6799
+ description: 'Simulate keyboard input in the running game via UserInputService:CreateVirtualInput. Use during a playtest for character movement (W/A/S/D walks at full WalkSpeed with player controls intact), jumping (Space), interactions (E), or any key-driven action. Drives the real input pipeline so game scripts and control modules respond. For sustained movement use action="press" to hold and "release" to let go. Pass "text" instead of keyCode to type a string into the focused TextBox. Auto-targets the running client; only works during a playtest.',
5359
6800
  inputSchema: {
5360
6801
  type: "object",
5361
6802
  properties: {
5362
6803
  keyCode: {
5363
6804
  type: "string",
5364
- description: 'Enum.KeyCode name: "W", "A", "S", "D", "Space", "E", "F", "LeftShift", "LeftControl", "Return", "Tab", "Escape", "One", "Two", etc.'
6805
+ description: 'Enum.KeyCode name: "W", "A", "S", "D", "Space", "E", "F", "LeftShift", "LeftControl", "Return", "Tab", "Escape", "One", "Two", etc. Omit if using "text".'
5365
6806
  },
5366
6807
  action: {
5367
6808
  type: "string",
@@ -5372,16 +6813,19 @@ part(0,2,0,2,1,1,"b")`,
5372
6813
  type: "number",
5373
6814
  description: 'Hold duration in seconds for "tap" action (default: 0.1). Use longer values for sustained input like walking.'
5374
6815
  },
6816
+ text: {
6817
+ type: "string",
6818
+ description: "Type this string into the currently focused TextBox (uses SendTextInput). When provided, keyCode/action are ignored."
6819
+ },
5375
6820
  target: {
5376
6821
  type: "string",
5377
- description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
6822
+ description: 'Instance target. Defaults to the running playtest client (client-1) when present, else "edit". Override with "server", "client-2", etc.'
5378
6823
  },
5379
6824
  instance_id: {
5380
6825
  type: "string",
5381
6826
  description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
5382
6827
  }
5383
- },
5384
- required: ["keyCode"]
6828
+ }
5385
6829
  }
5386
6830
  },
5387
6831
  // === Character Navigation ===
@@ -5749,7 +7193,7 @@ function handleVariantConflict({ pluginsFolder, otherAssetName, replace }) {
5749
7193
  }
5750
7194
  console.warn(`
5751
7195
  [install-plugin] WARNING: ${otherAssetName} is already present in ${pluginsFolder}.
5752
- Both plugins will register with MCP at Studio launch, causing duplicate role registrations and unpredictable routing for stop_playtest and per-peer execute_luau.
7196
+ Only one MCP plugin variant should be present. If both variants are in the Studio Plugins folder, Studio loads both and runtime routing can become unpredictable.
5753
7197
  Re-run with --replace-variant to remove ${otherAssetName}, or delete it manually.
5754
7198
  `);
5755
7199
  }