@el4cteo/rbx-studio-mcp 0.3.5 → 0.3.6

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.
@@ -20,8 +20,19 @@ import { defineTool } from "../lib/tool.js";
20
20
  * An earlier version of this file claimed the error was the ratio between the
21
21
  * device resolution and the screenshot's, which is wrong, and wrong in a way
22
22
  * that happens to land close to correct in the middle of the screen where it
23
- * was first observed. Reporting the measurement retires the guesswork: aim
24
- * once, read the delta, correct.
23
+ * was first observed.
24
+ *
25
+ * Applying it automatically was tried and reverted. The translation is real
26
+ * with nothing emulated -- (0,-58), which is exactly GuiService:GetGuiInset()
27
+ * -- and on an iPhone 16 in portrait, measured equal at two points 215px apart.
28
+ * On the SAME phone in LandscapeRight it is not a translation at all: the
29
+ * horizontal term inverts and scales (seen.x fitted 919 - 1.4 * sent.x), so
30
+ * subtracting the measured constant moved the aim from 187px out to 449px out.
31
+ *
32
+ * Nor can a better fit be sampled. Synthetic MOVES report no position --
33
+ * InputChanged never fires for SendMousePosition -- so the map is observable
34
+ * only by clicking, and probe clicks press whatever they land on. Aim once,
35
+ * read the delta, correct.
25
36
  */
26
37
  function landingNote(landed) {
27
38
  const dx = landed.seen.x - landed.sent.x;
@@ -31,7 +42,8 @@ function landingNote(landed) {
31
42
  }
32
43
  return (` COORDINATES ARE OFFSET: the last click was aimed at (${landed.sent.x}, ${landed.sent.y}) and the client read it at (${landed.seen.x}, ${landed.seen.y}) — ` +
33
44
  `off by (${dx}, ${dy}). Add (${-dx}, ${-dy}) to coordinates taken from a \`screenshot\` to hit what you are aiming at. ` +
34
- "The offset is constant across the viewport but changes with the device and orientation, so re-read it from this field after either changes.");
45
+ "That correction is a constant translation with no device emulated and in portrait, but NOT in landscape emulation, where the horizontal term inverts — " +
46
+ "so under an emulated device treat it as one sample: click, read this field again, and correct from the newest one rather than reusing an earlier value.");
35
47
  }
36
48
  export function registerInputTools(context) {
37
49
  const { bridge } = context;
@@ -64,7 +76,15 @@ export function registerInputTools(context) {
64
76
  "get an error, not a success — check where things really are with " +
65
77
  "`character op=\"state\"`.\n\n" +
66
78
  "Mouse coordinates are viewport pixels from the top-left, so pair this " +
67
- "with `screenshot` to see what is where before clicking it.",
79
+ "with `screenshot` to see what is where before clicking it. The client " +
80
+ "reads them at an offset the reply reports as `landed` — aim once, read " +
81
+ "where it actually landed, then correct. Under an emulated device that " +
82
+ "offset is large and stops being a simple translation, so re-read it " +
83
+ "after each click rather than reusing an earlier one.\n\n" +
84
+ "A `text` step types into the FOCUSED TextBox. Click the box in the same " +
85
+ "call, one step before the text, and the focus is taken for you; with no " +
86
+ "box to type into the step is reported as having done nothing rather " +
87
+ "than as delivered.",
68
88
  inputSchema: {
69
89
  steps: z
70
90
  .array(z
@@ -91,7 +111,10 @@ export function registerInputTools(context) {
91
111
  .describe("click only: which button. Defaults to left."),
92
112
  x: z.number().optional().describe("click/move only: viewport pixels from the left."),
93
113
  y: z.number().optional().describe("click/move only: viewport pixels from the top."),
94
- text: z.string().optional().describe("text only: the string to type."),
114
+ text: z
115
+ .string()
116
+ .optional()
117
+ .describe("text only: the string to type. Goes to the focused TextBox — click it first, in the same call."),
95
118
  hold: z
96
119
  .number()
97
120
  .min(0)
@@ -163,6 +186,16 @@ export function registerInputTools(context) {
163
186
  else {
164
187
  parts.push('Read the result with `character op="state"`, `console`, or `screenshot`.');
165
188
  }
189
+ /*
190
+ * A step that ran and changed nothing is the failure this whole file
191
+ * exists to avoid reporting as success, so the client's own words about
192
+ * it go at the front where they cannot be skimmed past.
193
+ */
194
+ if (Array.isArray(response.notes)) {
195
+ for (const line of response.notes) {
196
+ parts.unshift(`WARNING: ${line}`);
197
+ }
198
+ }
166
199
  const note = parts.join(" ");
167
200
  return json(response, note);
168
201
  });
@@ -1 +1 @@
1
- {"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@el4cteo/rbx-studio-mcp",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "MCP server for Roblox Studio. 29 tools, push-based SSE bridge, editor-safe script edits, one-step undo.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
 
10
10
  local Config = {}
11
11
 
12
- Config.PLUGIN_VERSION = "0.3.5"
12
+ Config.PLUGIN_VERSION = "0.3.6"
13
13
 
14
14
  -- Fingerprint of plugin/src, stamped in by scripts/build-plugin.mjs. The server
15
15
  -- computes the same hash from its own copy of the sources and compares, so a
@@ -49,6 +49,43 @@ local MAX_RECORDS = 300
49
49
  ]]
50
50
  local PALETTE: Themes.Palette = Themes.palette()
51
51
 
52
+ --[[
53
+ An optional listener on everything this console is told.
54
+
55
+ Exists for `Mirror`, which relays a playtest server's activity to the client
56
+ view that cannot reach the bridge itself. Kept as one hook rather than as
57
+ calls sprinkled through the file so there is a single place where "what the
58
+ console was told" is defined, and so the client half -- which sets no
59
+ observer -- cannot echo what it replays back into the channel.
60
+ ]]
61
+ local observer: ((string, { any }) -> ())? = nil
62
+
63
+ function Console.setObserver(fn: ((string, { any }) -> ())?)
64
+ observer = fn
65
+ end
66
+
67
+ local function notify(kind: string, arguments: { any })
68
+ local listener = observer
69
+ if listener ~= nil then
70
+ task.spawn(listener, kind, arguments)
71
+ end
72
+ end
73
+
74
+ --[[
75
+ Set on the console that is REPLAYING someone else's events.
76
+
77
+ A replayed call must not grow its own consequences. `recordCall` schedules
78
+ the "agent idle" summary, so a mirrored session scheduled one of its own and
79
+ then received the original over the channel too -- the same line twice, a
80
+ few pixels apart, which is exactly the sort of thing that makes a panel look
81
+ broken. The origin is the only session entitled to decide the burst ended.
82
+ ]]
83
+ local mirroring = false
84
+
85
+ function Console.setMirroring(value: boolean)
86
+ mirroring = value
87
+ end
88
+
52
89
  -- Sigil plus colour per level. The sigil is what makes the log scannable at a
53
90
  -- glance; colour alone fails for anyone who cannot separate red from green.
54
91
  -- Named by palette key rather than by colour, because this table is built once
@@ -385,6 +422,7 @@ function Console.log(level: Level, message: string, detail: string?)
385
422
  table.remove(state.records, 1)
386
423
  end
387
424
  redraw()
425
+ notify("log", { level, message, detail })
388
426
  end
389
427
 
390
428
  --[[
@@ -422,6 +460,7 @@ function Console.beginCall(title: string, kind: string)
422
460
  Visuals.setCaption(title)
423
461
  Visuals.setKind(PALETTE[look.key], look.urgency)
424
462
  refreshCounters()
463
+ notify("beginCall", { title, kind })
425
464
  end
426
465
 
427
466
  --[[
@@ -446,6 +485,7 @@ function Console.recordCall(ok: boolean, milliseconds: number)
446
485
  -- The band plots it: bar height is how long it took, colour is whether it
447
486
  -- worked, and a failure knocks the solid off its axis as well.
448
487
  Visuals.recordCall(milliseconds, ok, title)
488
+ notify("recordCall", { ok, milliseconds, title })
449
489
 
450
490
  --[[
451
491
  Says so when the session goes quiet, once per burst.
@@ -459,6 +499,9 @@ function Console.recordCall(ok: boolean, milliseconds: number)
459
499
  ]]
460
500
  state.generation += 1
461
501
  local mine = state.generation
502
+ if mirroring then
503
+ return
504
+ end
462
505
  task.delay(QUIET_SECONDS, function()
463
506
  if state.generation ~= mine or state.calls == 0 then
464
507
  return
@@ -604,7 +647,7 @@ function Console.setStatus(status: string, meta: string)
604
647
  local color = if status == "connected"
605
648
  then PALETTE.green
606
649
  elseif status == "connecting" then PALETTE.amber
607
- elseif status == "standby" then PALETTE.dim
650
+ elseif status == "standby" or status == "mirroring" then PALETTE.dim
608
651
  else PALETTE.red
609
652
 
610
653
  dot.TextColor3 = color
@@ -0,0 +1,126 @@
1
+ --!strict
2
+ --[[
3
+ Carries console activity from a playtest's SERVER view to its CLIENT view.
4
+
5
+ Pressing Play gives this plugin three lives: the editor's, the playtest
6
+ server's, and the playtest client's. Studio shows whichever belongs to the
7
+ view you are looking at, and it defaults to the client -- which is the one
8
+ life that can do nothing. Roblox forbids HTTP from a client session, so that
9
+ panel has no way to see the bridge, and it sat on a standby notice while the
10
+ agent worked a few feet away in a view the user was not looking at.
11
+
12
+ The two DataModels share no memory, no globals and no services. The only
13
+ channel between them is the game's own replication, so that is what this
14
+ uses: the server plugin puts a RemoteEvent in ReplicatedStorage and fires
15
+ each console event across it; the client plugin listens and replays them
16
+ into its own console. The prism reacts, the log fills, and the view the user
17
+ actually has open stops lying about what is happening.
18
+
19
+ The cost is honest and worth stating: this plugin writes one instance into
20
+ the running game. It is `Archivable = false` so it can never be saved with
21
+ the place, it exists only while a playtest is running, and it goes when the
22
+ playtest's DataModel does. Nothing reads it but the client half of this
23
+ plugin, and nothing is sent up from the client -- the traffic is one-way, so
24
+ a game's own scripts cannot use it to reach the editor.
25
+ ]]
26
+
27
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
28
+ local RunService = game:GetService("RunService")
29
+
30
+ --[[
31
+ Named to be recognised in the Explorer rather than to be hidden.
32
+
33
+ Someone will see this appear in ReplicatedStorage mid-playtest and want to
34
+ know what put it there; a cryptic name turns that into a hunt. The prefix is
35
+ unlikely enough to collide with a real game's instances.
36
+ ]]
37
+ local CHANNEL = "__rbxStudioMcpMirror"
38
+
39
+ local Mirror = {}
40
+
41
+ local remote: RemoteEvent? = nil
42
+
43
+ --[[
44
+ True only in the half of a playtest that can reach the bridge.
45
+
46
+ `IsRunning` separates a playtest from the editor, and `IsServer` separates
47
+ the two halves of it. The editor session must not do any of this: it has no
48
+ playtest to mirror and writing to its ReplicatedStorage would put the
49
+ instance in the user's actual place.
50
+ ]]
51
+ function Mirror.isPlaytestServer(): boolean
52
+ return RunService:IsRunning() and RunService:IsServer()
53
+ end
54
+
55
+ function Mirror.isPlaytestClient(): boolean
56
+ return RunService:IsRunning() and RunService:IsClient()
57
+ end
58
+
59
+ --[[
60
+ Opens the channel from the playtest server.
61
+
62
+ A leftover from an earlier test in the same DataModel is replaced rather
63
+ than reused: reusing one whose connections are gone looks like a working
64
+ channel that silently delivers nothing.
65
+ ]]
66
+ function Mirror.startServer()
67
+ if not Mirror.isPlaytestServer() then
68
+ return
69
+ end
70
+ local existing = ReplicatedStorage:FindFirstChild(CHANNEL)
71
+ if existing ~= nil then
72
+ existing:Destroy()
73
+ end
74
+ local event = Instance.new("RemoteEvent")
75
+ event.Name = CHANNEL
76
+ -- Never saved with the place, whatever the user does while the test runs.
77
+ event.Archivable = false
78
+ event.Parent = ReplicatedStorage
79
+ remote = event
80
+ end
81
+
82
+ --[[
83
+ Sends one console event, and never fails loudly.
84
+
85
+ This is decoration on top of the console. A replication error here must not
86
+ take down the call it was reporting on, so every send is guarded and a dead
87
+ channel simply stops mirroring.
88
+ ]]
89
+ function Mirror.send(kind: string, arguments: { any })
90
+ local event = remote
91
+ if event == nil then
92
+ return
93
+ end
94
+ pcall(function()
95
+ event:FireAllClients(kind, arguments)
96
+ end)
97
+ end
98
+
99
+ --[[
100
+ Listens on the playtest client.
101
+
102
+ `WaitForChild` with a timeout rather than an indefinite wait: if the server
103
+ half never opens the channel the client should go quiet, not hang a thread
104
+ for the length of the playtest.
105
+ ]]
106
+ function Mirror.startClient(onMessage: (string, { any }) -> ())
107
+ if not Mirror.isPlaytestClient() then
108
+ return
109
+ end
110
+ task.spawn(function()
111
+ local event = ReplicatedStorage:WaitForChild(CHANNEL, 30)
112
+ if event == nil or not event:IsA("RemoteEvent") then
113
+ return
114
+ end
115
+ event.OnClientEvent:Connect(function(kind, arguments)
116
+ -- Shape-checked because anything on the wire can be forged by a
117
+ -- script in the place being tested. Nothing here is trusted enough
118
+ -- to index without looking.
119
+ if typeof(kind) == "string" and typeof(arguments) == "table" then
120
+ onMessage(kind, arguments :: { any })
121
+ end
122
+ end)
123
+ end)
124
+ end
125
+
126
+ return Mirror