@chrrxs/robloxstudio-mcp 2.9.1 → 2.10.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.
@@ -1,4 +1,5 @@
1
1
  import { HttpService, Players, ReplicatedStorage, RunService } from "@rbxts/services";
2
+ import RuntimeLogBuffer from "./RuntimeLogBuffer";
2
3
 
3
4
  // The client peer cannot reach the MCP HTTP server - Roblox forbids
4
5
  // HttpService:RequestAsync from the client DM even under PluginSecurity, and
@@ -22,7 +23,12 @@ interface ProxyEntry {
22
23
  role: string;
23
24
  }
24
25
 
25
- interface ExecutePayload {
26
+ interface BrokerEnvelope {
27
+ endpoint?: string;
28
+ data?: Record<string, unknown>;
29
+ // Backward-compat: older server-broker code (pre-v2.10) sent the raw
30
+ // {code} payload directly. If we see code at the top level and no
31
+ // endpoint, treat it as execute-luau.
26
32
  code?: string;
27
33
  }
28
34
 
@@ -33,6 +39,14 @@ interface ExecuteResult {
33
39
  error?: string;
34
40
  }
35
41
 
42
+ // Endpoints the server-peer broker is allowed to forward to the client peer.
43
+ // Each requires the client peer's plugin VM (because the buffer / require
44
+ // cache / etc. lives there) so the server peer alone can't satisfy them.
45
+ const CLIENT_BROKER_ALLOWED_ENDPOINTS = new Set<string>([
46
+ "/api/execute-luau",
47
+ "/api/get-runtime-logs",
48
+ ]);
49
+
36
50
  interface ReadyResponseBody {
37
51
  assignedRole?: string;
38
52
  }
@@ -43,6 +57,22 @@ interface PollResponseBody {
43
57
  endpoint: string;
44
58
  data?: Record<string, unknown>;
45
59
  };
60
+ // Server signals knownInstance=false when our proxy isn't in its
61
+ // in-memory instances map (typically after an MCP process restart).
62
+ // Triggers a re-register POST to /ready.
63
+ knownInstance?: boolean;
64
+ }
65
+
66
+ // Throttle re-ready calls per proxyId so a brief window of unknownInstance
67
+ // polls doesn't cause a re-register stampede.
68
+ const lastReadyByProxy = new Map<string, number>();
69
+
70
+ function reRegisterProxy(proxyId: string, role: string): void {
71
+ const now = tick();
72
+ const last = lastReadyByProxy.get(proxyId) ?? 0;
73
+ if (now - last < 2) return;
74
+ lastReadyByProxy.set(proxyId, now);
75
+ pcall(() => postJson("/ready", { instanceId: proxyId, role }));
46
76
  }
47
77
 
48
78
  function forkRole(): "edit" | "server" | "client" {
@@ -62,37 +92,64 @@ function postJson(endpoint: string, body: Record<string, unknown>) {
62
92
  );
63
93
  }
64
94
 
95
+ function handleExecuteLuau(data: Record<string, unknown> | undefined): ExecuteResult {
96
+ const code = data && (data.code as string | undefined);
97
+ if (typeIs(code, "string") === false || code === "") {
98
+ return { success: false, error: "code is required" };
99
+ }
100
+ const m = new Instance("ModuleScript");
101
+ m.Name = "__MCPClientEval";
102
+ const [okSet, setErr] = pcall(() => {
103
+ (m as unknown as { Source: string }).Source = code as string;
104
+ });
105
+ if (!okSet) {
106
+ m.Destroy();
107
+ return { success: false, error: `Source set failed: ${tostring(setErr)}` };
108
+ }
109
+ m.Parent = game.Workspace;
110
+ const [okReq, result] = pcall(() => require(m));
111
+ m.Destroy();
112
+ if (okReq) {
113
+ return {
114
+ success: true,
115
+ returnValue: result !== undefined ? tostring(result) : undefined,
116
+ message: "Code executed successfully",
117
+ };
118
+ }
119
+ return { success: false, error: tostring(result) };
120
+ }
121
+
122
+ function handleGetRuntimeLogs(data: Record<string, unknown> | undefined): unknown {
123
+ const d = data ?? {};
124
+ const since = d.since as number | undefined;
125
+ const tail = d.tail as number | undefined;
126
+ const filter = d.filter as string | undefined;
127
+ // "client" is the generic peer tag; MCP-side aggregator overrides with
128
+ // the specific role (e.g. "client-1") on target=all fan-out.
129
+ return RuntimeLogBuffer.query({ since, tail, filter }, "client");
130
+ }
131
+
65
132
  function setupClientBroker() {
66
133
  const rf = ReplicatedStorage.WaitForChild(BROKER_NAME, 10);
67
134
  if (!rf || !rf.IsA("RemoteFunction")) {
68
135
  warn(`[MCPFork] client: ${BROKER_NAME} not found`);
69
136
  return;
70
137
  }
71
- rf.OnClientInvoke = (payload: ExecutePayload | undefined) => {
72
- const code = payload && payload.code;
73
- if (typeIs(code, "string") === false || code === "") {
74
- return identity<ExecuteResult>({ success: false, error: "code is required" });
138
+ rf.OnClientInvoke = (payload: BrokerEnvelope | undefined) => {
139
+ // Two payload shapes in the wild:
140
+ // - {endpoint, data} from v2.10+ server-peer broker (this is the new
141
+ // discriminated form that lets us dispatch on endpoint)
142
+ // - {code} from pre-v2.10 server-peer broker (raw execute-luau payload)
143
+ // The shapes coexist gracefully because we fall back to execute-luau
144
+ // when endpoint is missing.
145
+ if (payload && payload.endpoint === "/api/get-runtime-logs") {
146
+ return handleGetRuntimeLogs(payload.data);
75
147
  }
76
- const m = new Instance("ModuleScript");
77
- m.Name = "__MCPClientEval";
78
- const [okSet, setErr] = pcall(() => {
79
- (m as unknown as { Source: string }).Source = code as string;
80
- });
81
- if (!okSet) {
82
- m.Destroy();
83
- return identity<ExecuteResult>({ success: false, error: `Source set failed: ${tostring(setErr)}` });
84
- }
85
- m.Parent = game.Workspace;
86
- const [okReq, result] = pcall(() => require(m));
87
- m.Destroy();
88
- if (okReq) {
89
- return identity<ExecuteResult>({
90
- success: true,
91
- returnValue: result !== undefined ? tostring(result) : undefined,
92
- message: "Code executed successfully",
93
- });
148
+ if (payload && payload.endpoint === "/api/execute-luau") {
149
+ return handleExecuteLuau(payload.data);
94
150
  }
95
- return identity<ExecuteResult>({ success: false, error: tostring(result) });
151
+ // Legacy: raw execute-luau payload at the top level.
152
+ return handleExecuteLuau(payload as Record<string, unknown> | undefined);
96
153
  };
97
154
  }
98
155
 
@@ -109,20 +166,34 @@ function pollProxy(proxyId: string, player: Player, rf: RemoteFunction) {
109
166
  );
110
167
  if (ok && res && (res.Success || res.StatusCode === 503)) {
111
168
  const [okJson, body] = pcall(() => HttpService.JSONDecode(res.Body) as PollResponseBody);
112
- if (okJson && body && body.request && body.requestId !== undefined) {
113
- const request = body.request;
114
- let response: unknown;
115
- if (request.endpoint === "/api/execute-luau") {
116
- const [okInvoke, invokeRes] = pcall(() => rf.InvokeClient(player, request.data));
117
- if (okInvoke) {
118
- response = invokeRes !== undefined ? invokeRes : { success: false, error: "nil response" };
169
+ if (okJson && body) {
170
+ // Server lost our proxy registration (process restart, etc.) -
171
+ // re-register so the next poll cycle starts routing again.
172
+ if (body.knownInstance === false) {
173
+ reRegisterProxy(proxyId, "client");
174
+ }
175
+ if (body.request && body.requestId !== undefined) {
176
+ const request = body.request;
177
+ let response: unknown;
178
+ if (CLIENT_BROKER_ALLOWED_ENDPOINTS.has(request.endpoint)) {
179
+ // Forward as a discriminated envelope so the client-side
180
+ // OnClientInvoke knows which endpoint it's serving.
181
+ const envelope = { endpoint: request.endpoint, data: request.data };
182
+ const [okInvoke, invokeRes] = pcall(() => rf.InvokeClient(player, envelope));
183
+ if (okInvoke) {
184
+ response = invokeRes !== undefined ? invokeRes : { success: false, error: "nil response" };
185
+ } else {
186
+ response = { success: false, error: `InvokeClient failed: ${tostring(invokeRes)}` };
187
+ }
119
188
  } else {
120
- response = { success: false, error: `InvokeClient failed: ${tostring(invokeRes)}` };
189
+ response = {
190
+ error:
191
+ `Client-proxy does not forward ${tostring(request.endpoint)}. ` +
192
+ `Allowed: /api/execute-luau, /api/get-runtime-logs.`,
193
+ };
121
194
  }
122
- } else {
123
- response = { error: `Client-proxy only supports /api/execute-luau, got: ${tostring(request.endpoint)}` };
195
+ postJson("/response", { requestId: body.requestId, response });
124
196
  }
125
- postJson("/response", { requestId: body.requestId, response });
126
197
  }
127
198
  }
128
199
  task.wait(0.5);
@@ -161,19 +232,25 @@ function startEditProxyLoop() {
161
232
  );
162
233
  if (okPoll && pollRes && (pollRes.Success || pollRes.StatusCode === 503)) {
163
234
  const [okJson, body] = pcall(() => HttpService.JSONDecode(pollRes.Body) as PollResponseBody);
164
- if (
165
- okJson &&
166
- body &&
167
- body.request &&
168
- body.request.endpoint === "/api/stop-playtest" &&
169
- body.requestId !== undefined
170
- ) {
171
- const sts = game.GetService("StudioTestService") as Instance & { EndTest(reason: string): void };
172
- const [endOk, endErr] = pcall(() => sts.EndTest("stopped_by_mcp"));
173
- const response = endOk
174
- ? { success: true, message: "Playtest stopped via edit-proxy/EndTest" }
175
- : { success: false, error: `EndTest failed: ${tostring(endErr)}` };
176
- postJson("/response", { requestId: body.requestId, response });
235
+ if (okJson && body) {
236
+ // Re-register if the server lost our edit-proxy registration.
237
+ if (body.knownInstance === false) {
238
+ reRegisterProxy(proxyId, "edit-proxy");
239
+ }
240
+ if (
241
+ body.request &&
242
+ body.request.endpoint === "/api/stop-playtest" &&
243
+ body.requestId !== undefined
244
+ ) {
245
+ const sts = game.GetService("StudioTestService") as Instance & {
246
+ EndTest(reason: string): void;
247
+ };
248
+ const [endOk, endErr] = pcall(() => sts.EndTest("stopped_by_mcp"));
249
+ const response = endOk
250
+ ? { success: true, message: "Playtest stopped via edit-proxy/EndTest" }
251
+ : { success: false, error: `EndTest failed: ${tostring(endErr)}` };
252
+ postJson("/response", { requestId: body.requestId, response });
253
+ }
177
254
  }
178
255
  }
179
256
  task.wait(0.15);
@@ -12,6 +12,7 @@ import BuildHandlers from "./handlers/BuildHandlers";
12
12
  import AssetHandlers from "./handlers/AssetHandlers";
13
13
  import CaptureHandlers from "./handlers/CaptureHandlers";
14
14
  import InputHandlers from "./handlers/InputHandlers";
15
+ import LogHandlers from "./handlers/LogHandlers";
15
16
  import { Connection, RequestPayload, PollResponse, ReadyResponse } from "../types";
16
17
 
17
18
  const instanceId = HttpService.GenerateGUID(false);
@@ -92,6 +93,8 @@ const routeMap: Record<string, Handler> = {
92
93
  "/api/simulate-keyboard-input": InputHandlers.simulateKeyboardInput,
93
94
 
94
95
  "/api/find-and-replace-in-scripts": ScriptHandlers.findAndReplaceInScripts,
96
+
97
+ "/api/get-runtime-logs": LogHandlers.getRuntimeLogs,
95
98
  };
96
99
 
97
100
  function processRequest(request: RequestPayload): unknown {
@@ -125,6 +128,40 @@ function getConnectionStatus(connIndex: number): string {
125
128
  return "connecting";
126
129
  }
127
130
 
131
+ // Throttle for re-issuing /ready after the server reports knownInstance=false.
132
+ // Without this, every poll during the brief window where the server has just
133
+ // restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
134
+ let lastReadyPostAt = 0;
135
+
136
+ function sendReady(conn: Connection): void {
137
+ const now = tick();
138
+ if (now - lastReadyPostAt < 2) return; // throttle to ≤1 /ready every 2s
139
+ lastReadyPostAt = now;
140
+ task.spawn(() => {
141
+ const [readyOk, readyResult] = pcall(() => {
142
+ return HttpService.RequestAsync({
143
+ Url: `${conn.serverUrl}/ready`,
144
+ Method: "POST",
145
+ Headers: { "Content-Type": "application/json" },
146
+ Body: HttpService.JSONEncode({
147
+ instanceId,
148
+ role: detectRole(),
149
+ pluginReady: true,
150
+ timestamp: tick(),
151
+ }),
152
+ });
153
+ });
154
+ if (readyOk && readyResult.Success) {
155
+ const [parseOk, readyData] = pcall(
156
+ () => HttpService.JSONDecode(readyResult.Body) as ReadyResponse,
157
+ );
158
+ if (parseOk && readyData.assignedRole) {
159
+ assignedRole = readyData.assignedRole;
160
+ }
161
+ }
162
+ });
163
+ }
164
+
128
165
  function pollForRequests(connIndex: number) {
129
166
  const conn = State.getConnection(connIndex);
130
167
  if (!conn || !conn.isActive) return;
@@ -155,6 +192,15 @@ function pollForRequests(connIndex: number) {
155
192
  conn.lastHttpOk = true;
156
193
  conn.lastMcpOk = mcpConnected;
157
194
 
195
+ // Server tells us when its in-memory instances map doesn't have us
196
+ // (e.g. after an MCP process restart). Re-issue /ready immediately so
197
+ // target=server/client-N start routing again. The throttle inside
198
+ // sendReady() prevents duplicate registrations while the server
199
+ // catches up.
200
+ if (data.knownInstance === false) {
201
+ sendReady(conn);
202
+ }
203
+
158
204
  if (connIndex === State.getActiveTabIndex()) {
159
205
  const el = ui;
160
206
  el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
@@ -298,33 +344,20 @@ function activatePlugin(connIndex?: number) {
298
344
  }
299
345
  UI.updateTabDot(idx);
300
346
 
301
- task.spawn(() => {
302
- if (!conn.heartbeatConnection) {
303
- conn.heartbeatConnection = RunService.Heartbeat.Connect(() => {
304
- const now = tick();
305
- const currentInterval = conn.consecutiveFailures > 5 ? conn.currentRetryDelay : conn.pollInterval;
306
- if (now - conn.lastPoll > currentInterval) {
307
- conn.lastPoll = now;
308
- pollForRequests(idx);
309
- }
310
- });
311
- }
312
-
313
- const [readyOk, readyResult] = pcall(() => {
314
- return HttpService.RequestAsync({
315
- Url: `${conn.serverUrl}/ready`,
316
- Method: "POST",
317
- Headers: { "Content-Type": "application/json" },
318
- Body: HttpService.JSONEncode({ instanceId, role: detectRole(), pluginReady: true, timestamp: tick() }),
319
- });
320
- });
321
- if (readyOk && readyResult.Success) {
322
- const [parseOk, readyData] = pcall(() => HttpService.JSONDecode(readyResult.Body) as ReadyResponse);
323
- if (parseOk && readyData.assignedRole) {
324
- assignedRole = readyData.assignedRole;
347
+ if (!conn.heartbeatConnection) {
348
+ conn.heartbeatConnection = RunService.Heartbeat.Connect(() => {
349
+ const now = tick();
350
+ const currentInterval = conn.consecutiveFailures > 5 ? conn.currentRetryDelay : conn.pollInterval;
351
+ if (now - conn.lastPoll > currentInterval) {
352
+ conn.lastPoll = now;
353
+ pollForRequests(idx);
325
354
  }
326
- }
327
- });
355
+ });
356
+ }
357
+
358
+ // Initial /ready; pollForRequests will also re-fire ready if the server
359
+ // later reports knownInstance=false (process restart, etc).
360
+ sendReady(conn);
328
361
  }
329
362
 
330
363
  function deactivatePlugin(connIndex?: number) {
@@ -0,0 +1,138 @@
1
+ // Per-peer in-memory ring buffer for LogService.MessageOut events.
2
+ // Powers the get_runtime_logs MCP tool. Replaces the out-of-tree LogBuffer
3
+ // primitives + StringValue approach from chrrxs/roblox-mcp-primitives.
4
+ //
5
+ // Each peer's plugin attaches a MessageOut listener at plugin load (edit DM,
6
+ // play-server DM, play-client DM all run their own copy of this module).
7
+ // Captured entries live in plugin module-state; nothing is parented to the
8
+ // DataModel. The buffer is bounded by a message-byte budget; oldest entries
9
+ // drop when over budget.
10
+ //
11
+ // Peer-tag caveat: returned entries reflect which peer's plugin CAPTURED the
12
+ // entry, NOT which peer's script originated the print. LogService reflects
13
+ // prints across peers in Studio Play (a server print ends up in both the
14
+ // server and client LogService:GetLogHistory()) and origin is empirically
15
+ // undetectable from inside MessageOut. The MCP-side aggregator handles
16
+ // cross-peer dedup via a 2s timestamp window.
17
+
18
+ import { LogService, RunService } from "@rbxts/services";
19
+
20
+ type LogLevel = "OUT" | "WARN" | "ERR" | "INFO";
21
+
22
+ interface RuntimeLogEntry {
23
+ seq: number;
24
+ ts: number; // wall-clock seconds via DateTime, coherent across peers
25
+ level: LogLevel;
26
+ message: string;
27
+ }
28
+
29
+ const MAX_BYTES = 64 * 1024;
30
+ const HARD_ENTRY_CAP = 50_000;
31
+
32
+ const entries: RuntimeLogEntry[] = [];
33
+ let totalBytes = 0;
34
+ let totalDropped = 0;
35
+ let nextSeq = 1;
36
+ let installed = false;
37
+
38
+ function levelTag(t: Enum.MessageType): LogLevel {
39
+ if (t === Enum.MessageType.MessageWarning) return "WARN";
40
+ if (t === Enum.MessageType.MessageError) return "ERR";
41
+ if (t === Enum.MessageType.MessageInfo) return "INFO";
42
+ return "OUT";
43
+ }
44
+
45
+ function nowSec(): number {
46
+ return DateTime.now().UnixTimestampMillis / 1000;
47
+ }
48
+
49
+ function dropOldestUntilFits(incomingBytes: number): void {
50
+ while (
51
+ entries.size() > 0 &&
52
+ (totalBytes + incomingBytes > MAX_BYTES || entries.size() >= HARD_ENTRY_CAP)
53
+ ) {
54
+ const dropped = entries.shift()!;
55
+ totalBytes -= dropped.message.size();
56
+ totalDropped += 1;
57
+ }
58
+ }
59
+
60
+ function install(): void {
61
+ if (installed) return;
62
+ if (!RunService.IsStudio()) return;
63
+ installed = true;
64
+ LogService.MessageOut.Connect((msg, t) => {
65
+ const bytes = msg.size();
66
+ dropOldestUntilFits(bytes);
67
+ entries.push({
68
+ seq: nextSeq,
69
+ ts: nowSec(),
70
+ level: levelTag(t),
71
+ message: msg,
72
+ });
73
+ nextSeq += 1;
74
+ totalBytes += bytes;
75
+ });
76
+ }
77
+
78
+ function detectPeer(): "edit" | "server" | "client" {
79
+ if (!RunService.IsRunning()) return "edit";
80
+ if (RunService.IsServer()) return "server";
81
+ return "client";
82
+ }
83
+
84
+ interface QueryOptions {
85
+ since?: number;
86
+ tail?: number;
87
+ filter?: string; // Plain substring match, applied to message
88
+ }
89
+
90
+ interface QueryResult {
91
+ peer: string;
92
+ entries: RuntimeLogEntry[];
93
+ totalDropped: number;
94
+ nextSince: number;
95
+ }
96
+
97
+ function query(opts: QueryOptions, peer: string): QueryResult {
98
+ let result = opts.since !== undefined
99
+ ? entries.filter((e) => e.seq > (opts.since as number))
100
+ : [...entries];
101
+
102
+ if (opts.filter !== undefined) {
103
+ // Plain substring search (4th arg = true). Pattern matching here was
104
+ // surprising in practice - Lua magic chars in messages would silently
105
+ // not match (e.g. filter="MARK-EDIT" against "MARK-EDIT-001" fails
106
+ // because '-' means "0+" in Lua patterns). Substring search matches
107
+ // most users' mental model of "filter messages containing this text".
108
+ const needle = opts.filter;
109
+ result = result.filter((e) => {
110
+ const [start] = string.find(e.message, needle, 1, true);
111
+ return start !== undefined;
112
+ });
113
+ }
114
+
115
+ if (opts.tail !== undefined && result.size() > opts.tail) {
116
+ // roblox-ts arrays don't expose .slice; manual tail copy.
117
+ const tailed: RuntimeLogEntry[] = [];
118
+ const start = result.size() - opts.tail;
119
+ for (let i = start; i < result.size(); i++) {
120
+ tailed.push(result[i]);
121
+ }
122
+ result = tailed;
123
+ }
124
+
125
+ const last = entries.size() > 0 ? entries[entries.size() - 1] : undefined;
126
+ return {
127
+ peer,
128
+ entries: result,
129
+ totalDropped,
130
+ nextSince: last ? last.seq : (opts.since ?? 0),
131
+ };
132
+ }
133
+
134
+ export = {
135
+ install,
136
+ detectPeer,
137
+ query,
138
+ };
@@ -0,0 +1,15 @@
1
+ import RuntimeLogBuffer from "../RuntimeLogBuffer";
2
+
3
+ function getRuntimeLogs(requestData: Record<string, unknown>): unknown {
4
+ const since = requestData.since as number | undefined;
5
+ const tail = requestData.tail as number | undefined;
6
+ const filter = requestData.filter as string | undefined;
7
+ // Plugin-side peer tag is generic ("edit"|"server"|"client"). The MCP-side
8
+ // aggregator overrides it with the specific instance role (e.g. "client-1")
9
+ // during fan-out for target=all, so this value is only authoritative for
10
+ // the single-peer query path.
11
+ const peer = RuntimeLogBuffer.detectPeer();
12
+ return RuntimeLogBuffer.query({ since, tail, filter }, peer);
13
+ }
14
+
15
+ export = { getRuntimeLogs };
@@ -294,8 +294,19 @@ function executeLuau(requestData: Record<string, unknown>) {
294
294
  const runViaModuleScript = () => {
295
295
  const m = new Instance("ModuleScript");
296
296
  m.Name = "__MCPExecLuauPayload";
297
+ // Wrap user code in an IIFE so require() always gets exactly one
298
+ // return value. Without this, code like `print("x")` errors with
299
+ // "Module code did not return exactly one value" because top-level
300
+ // ModuleScripts must return exactly one value.
301
+ //
302
+ // The DOUBLE parens around the call are load-bearing: in Luau,
303
+ // `return f()` propagates whatever multi-value tuple f returns,
304
+ // including zero values. Outer parens adjust the call to exactly
305
+ // one value (the first, or nil). So `return ((f)())` always
306
+ // returns exactly one value, regardless of what f does.
307
+ const wrapped = `return ((function()\n${code}\nend)())`;
297
308
  const [okSet, setErr] = pcall(() => {
298
- (m as unknown as { Source: string }).Source = code;
309
+ (m as unknown as { Source: string }).Source = wrapped;
299
310
  });
300
311
  if (!okSet) {
301
312
  m.Destroy();
@@ -2,7 +2,12 @@ import State from "../modules/State";
2
2
  import UI from "../modules/UI";
3
3
  import Communication from "../modules/Communication";
4
4
  import ClientBroker from "../modules/ClientBroker";
5
+ import RuntimeLogBuffer from "../modules/RuntimeLogBuffer";
5
6
 
7
+ // Attach the per-peer LogService.MessageOut listener as early as possible so
8
+ // boot-time prints from the user's place scripts are captured. Powers the
9
+ // get_runtime_logs MCP tool. Idempotent; safe to call before UI.init().
10
+ RuntimeLogBuffer.install();
6
11
 
7
12
  UI.init(plugin);
8
13
  const elements = UI.getElements();
@@ -32,6 +32,10 @@ export interface PollResponse {
32
32
  mcpConnected: boolean;
33
33
  request?: RequestPayload;
34
34
  requestId?: string;
35
+ // Server signals knownInstance=false when its in-memory instances map
36
+ // doesn't contain our instanceId (typically after an MCP process restart).
37
+ // The plugin re-issues /ready when it sees this.
38
+ knownInstance?: boolean;
35
39
  }
36
40
 
37
41
  export interface ReadyResponse {