@princeofscale/bloxforge-inspector 3.0.0-rc.1 → 3.0.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.
@@ -75,6 +75,7 @@ const BROKER_OWNER_ATTRIBUTE = "__MCPBrokerOwner";
75
75
  interface ProxyEntry {
76
76
  pluginSessionId: string;
77
77
  role: string;
78
+ sessionToken: string;
78
79
  }
79
80
 
80
81
  interface BrokerEnvelope {
@@ -111,10 +112,14 @@ const CLIENT_BROKER_ALLOWED_ENDPOINTS = new Set<string>([
111
112
 
112
113
  interface ReadyResponseBody {
113
114
  assignedRole?: string;
115
+ sessionToken?: string;
114
116
  }
115
117
 
116
118
  interface PollResponseBody {
117
119
  requestId?: string;
120
+ serverEpoch?: string;
121
+ deliveryAttempt?: number;
122
+ leaseToken?: string;
118
123
  request?: {
119
124
  endpoint: string;
120
125
  data?: Record<string, unknown>;
@@ -125,17 +130,42 @@ interface PollResponseBody {
125
130
  knownInstance?: boolean;
126
131
  }
127
132
 
133
+ interface DeliveryFence {
134
+ serverEpoch: string;
135
+ deliveryAttempt: number;
136
+ leaseToken: string;
137
+ }
138
+
128
139
  // Throttle re-ready calls per proxyId so a brief window of unknownInstance
129
140
  // polls doesn't cause a re-register stampede.
130
141
  const lastReadyByProxy = new Map<string, number>();
142
+ const completedProxyRequests = new Map<string, { response: unknown }>();
143
+ const completedProxyRequestOrder: string[] = [];
144
+ const COMPLETED_PROXY_REQUEST_LIMIT = 500;
145
+
146
+ function rememberCompletedProxyRequest(requestId: string, response: unknown): void {
147
+ completedProxyRequests.set(requestId, { response });
148
+ completedProxyRequestOrder.push(requestId);
149
+ if (completedProxyRequestOrder.size() > COMPLETED_PROXY_REQUEST_LIMIT) {
150
+ completedProxyRequests.delete(completedProxyRequestOrder.shift()!);
151
+ }
152
+ }
153
+
154
+ function sendProxyResponse(requestId: string, response: unknown, proxy: ProxyEntry, fence: DeliveryFence): void {
155
+ const [ok, result] = postJson("/response", { requestId, pluginSessionId: proxy.pluginSessionId, response, ...fence }, proxy.sessionToken);
156
+ if (!ok || !result || !result.Success) {
157
+ warn(`[BloxForge] proxy response delivery failed: ${formatPostJsonFailure("/response", ok, result)}`);
158
+ }
159
+ }
131
160
 
132
- function reRegisterProxy(proxyId: string, role: string): void {
161
+ function reRegisterProxy(proxy: ProxyEntry, role: string): void {
162
+ const proxyId = proxy.pluginSessionId;
133
163
  const now = tick();
134
164
  const last = lastReadyByProxy.get(proxyId) ?? 0;
135
165
  if (now - last < 2) return;
136
166
  lastReadyByProxy.set(proxyId, now);
137
- pcall(() =>
138
- postJson("/ready", {
167
+ pcall(() => {
168
+ const [ok, result] = postJson("/ready", {
139
169
  pluginSessionId: proxyId,
140
170
  instanceId: computeInstanceId(),
141
171
  role,
@@ -146,8 +176,12 @@ function reRegisterProxy(proxyId: string, role: string): void {
146
176
  pluginVersion: State.CURRENT_VERSION,
147
177
  pluginVariant: State.PLUGIN_VARIANT,
148
178
  protocolVersion: State.PROTOCOL_VERSION,
149
- }),
150
- );
179
+ });
180
+ if (ok && result && result.Success) {
181
+ const body = HttpService.JSONDecode(result.Body) as ReadyResponseBody;
182
+ if (body.sessionToken !== undefined) proxy.sessionToken = body.sessionToken;
183
+ }
184
+ });
151
185
  }
152
186
 
153
187
  function forkRole(): "edit" | "server" | "client" {
@@ -156,12 +190,14 @@ function forkRole(): "edit" | "server" | "client" {
156
190
  return "client";
157
191
  }
158
192
 
159
- function postJson(endpoint: string, body: Record<string, unknown>) {
193
+ function postJson(endpoint: string, body: Record<string, unknown>, sessionToken?: string) {
194
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
195
+ if (sessionToken !== undefined && sessionToken !== "") headers.Authorization = `Bearer ${sessionToken}`;
160
196
  return pcall(() =>
161
197
  HttpService.RequestAsync({
162
198
  Url: `${mcpUrl}${endpoint}`,
163
199
  Method: "POST",
164
- Headers: { "Content-Type": "application/json" },
200
+ Headers: headers,
165
201
  Body: HttpService.JSONEncode(body),
166
202
  }),
167
203
  );
@@ -312,7 +348,7 @@ function unregisterProxy(player: Player, entry?: ProxyEntry): void {
312
348
  if (!proxy) return;
313
349
  proxyByPlayer.delete(player);
314
350
  proxyRegisterFailuresByPlayer.delete(player);
315
- postJson("/disconnect", { pluginSessionId: proxy.pluginSessionId });
351
+ postJson("/disconnect", { pluginSessionId: proxy.pluginSessionId }, proxy.sessionToken);
316
352
  }
317
353
 
318
354
  function disconnectAllProxies(): void {
@@ -323,7 +359,8 @@ function disconnectAllProxies(): void {
323
359
  proxyRegisterFailuresByPlayer.clear();
324
360
  }
325
361
 
326
- function pollProxy(proxyId: string, player: Player, rf: RemoteFunction) {
362
+ function pollProxy(proxy: ProxyEntry, player: Player, rf: RemoteFunction) {
363
+ const proxyId = proxy.pluginSessionId;
327
364
  while (player.Parent !== undefined && proxyByPlayer.has(player)) {
328
365
  if (!RunService.IsRunning()) {
329
366
  unregisterProxy(player);
@@ -333,18 +370,32 @@ function pollProxy(proxyId: string, player: Player, rf: RemoteFunction) {
333
370
  HttpService.RequestAsync({
334
371
  Url: `${mcpUrl}/poll?pluginSessionId=${proxyId}`,
335
372
  Method: "GET",
336
- Headers: { "Content-Type": "application/json" },
373
+ Headers: proxy.sessionToken !== ""
374
+ ? { "Content-Type": "application/json", Authorization: `Bearer ${proxy.sessionToken}` }
375
+ : { "Content-Type": "application/json" },
337
376
  }),
338
377
  );
378
+ if (ok && res && res.StatusCode === 401) {
379
+ reRegisterProxy(proxy, "client");
380
+ }
339
381
  if (ok && res && (res.Success || res.StatusCode === 503)) {
340
382
  const [okJson, body] = pcall(() => HttpService.JSONDecode(res.Body) as PollResponseBody);
341
383
  if (okJson && body) {
342
384
  // Server lost our proxy registration (process restart, etc.) -
343
385
  // re-register so the next poll cycle starts routing again.
344
386
  if (body.knownInstance === false) {
345
- reRegisterProxy(proxyId, "client");
387
+ reRegisterProxy(proxy, "client");
346
388
  }
347
389
  if (body.request && body.requestId !== undefined) {
390
+ if (!body.serverEpoch || body.deliveryAttempt === undefined || !body.leaseToken) continue;
391
+ const fence = { serverEpoch: body.serverEpoch, deliveryAttempt: body.deliveryAttempt, leaseToken: body.leaseToken };
392
+ const cached = completedProxyRequests.get(body.requestId);
393
+ if (cached !== undefined) {
394
+ sendProxyResponse(body.requestId, cached.response, proxy, fence);
395
+ task.wait(0.5);
396
+ continue;
397
+ }
398
+ postJson("/ack", { requestId: body.requestId, pluginSessionId: proxyId, ...fence }, proxy.sessionToken);
348
399
  const request = body.request;
349
400
  let response: unknown;
350
401
  if (CLIENT_BROKER_ALLOWED_ENDPOINTS.has(request.endpoint)) {
@@ -366,7 +417,8 @@ function pollProxy(proxyId: string, player: Player, rf: RemoteFunction) {
366
417
  `Allowed: ${allowed.join(", ")}.`,
367
418
  };
368
419
  }
369
- postJson("/response", { requestId: body.requestId, response });
420
+ rememberCompletedProxyRequest(body.requestId, response);
421
+ sendProxyResponse(body.requestId, response, proxy, fence);
370
422
  }
371
423
  }
372
424
  }
@@ -396,12 +448,13 @@ function registerProxy(player: Player, rf: RemoteFunction) {
396
448
  }
397
449
  const body = HttpService.JSONDecode(res.Body) as ReadyResponseBody;
398
450
  const assigned = body.assignedRole ?? "client";
399
- proxyByPlayer.set(player, { pluginSessionId: proxyId, role: assigned });
451
+ const entry = { pluginSessionId: proxyId, role: assigned, sessionToken: body.sessionToken ?? "" };
452
+ proxyByPlayer.set(player, entry);
400
453
  if (proxyRegisterFailuresByPlayer.has(player)) {
401
454
  proxyRegisterFailuresByPlayer.delete(player);
402
455
  print(`[BloxForge] proxy registered for ${player.Name} as ${assigned} via ${mcpUrl}`);
403
456
  }
404
- task.spawn(pollProxy, proxyId, player, rf);
457
+ task.spawn(pollProxy, entry, player, rf);
405
458
  }
406
459
 
407
460
  // (Removed: startEditProxyLoop. The play-server DM no longer registers an
@@ -98,6 +98,20 @@ function detectRole(): string {
98
98
 
99
99
  const initialRole = detectRole();
100
100
 
101
+ const activeRequests = new Set<string>();
102
+ const completedRequests = new Map<string, { response: unknown }>();
103
+ const completedRequestOrder: string[] = [];
104
+ const COMPLETED_REQUEST_LIMIT = 500;
105
+
106
+ const syncThreadRequests = new Map<thread, string>();
107
+ const cancelledSyncRequests = new Set<string>();
108
+
109
+ function isCancelledForThread(co: thread): boolean {
110
+ const reqId = syncThreadRequests.get(co);
111
+ if (reqId === undefined) return false;
112
+ return cancelledSyncRequests.has(reqId);
113
+ }
114
+
101
115
  type Handler = (data: Record<string, unknown>) => unknown;
102
116
 
103
117
  const routeMap: Record<string, Handler> = {
@@ -202,14 +216,92 @@ function processRequest(request: RequestPayload): unknown {
202
216
  }
203
217
  }
204
218
 
205
- function sendResponse(conn: Connection, requestId: string, responseData: unknown) {
206
- pcall(() => {
207
- HttpService.RequestAsync({
219
+ function sendResponse(conn: Connection, requestId: string, responseData: unknown, serverEpoch?: string, deliveryAttempt?: number, leaseToken?: string) {
220
+ const body: Record<string, unknown> = { requestId, response: responseData };
221
+ if (serverEpoch !== undefined) body.serverEpoch = serverEpoch;
222
+ if (deliveryAttempt !== undefined) body.deliveryAttempt = deliveryAttempt;
223
+ if (leaseToken !== undefined) body.leaseToken = leaseToken;
224
+
225
+ const [ok, result] = pcall(() => {
226
+ return HttpService.RequestAsync({
208
227
  Url: `${conn.serverUrl}/response`,
209
228
  Method: "POST",
210
- Headers: { "Content-Type": "application/json" },
211
- Body: HttpService.JSONEncode({ requestId, response: responseData }),
229
+ Headers: {
230
+ "Content-Type": "application/json",
231
+ ...(conn.sessionToken ? { "Authorization": `Bearer ${conn.sessionToken}` } : {})
232
+ },
233
+ Body: HttpService.JSONEncode(body),
234
+ });
235
+ });
236
+
237
+ if (!ok) {
238
+ warn(`[BloxForge] Plugin response serialization failed for ${requestId}: ${tostring(result)}`);
239
+ } else if (!result.Success) {
240
+ warn(`[BloxForge] Failed to deliver response for ${requestId}: HTTP ${result.StatusCode}`);
241
+ }
242
+ }
243
+
244
+ function handleRequestOnce(
245
+ conn: Connection,
246
+ requestId: string,
247
+ request: RequestPayload,
248
+ serverEpoch?: string,
249
+ deliveryAttempt?: number,
250
+ leaseToken?: string
251
+ ) {
252
+ // If we've already completed this request, just re-deliver the cached response
253
+ const completed = completedRequests.get(requestId);
254
+ if (completed) {
255
+ sendResponse(conn, requestId, completed.response, serverEpoch, deliveryAttempt, leaseToken);
256
+ return;
257
+ }
258
+
259
+ // If it's actively being processed, do nothing. The running thread will deliver
260
+ // the response when it finishes.
261
+ if (activeRequests.has(requestId)) return;
262
+
263
+ // Acknowledge the fenced request so the server knows we've started work.
264
+ // This prevents the lease from expiring while we're executing long-running operations.
265
+ if (serverEpoch !== undefined && deliveryAttempt !== undefined && leaseToken !== undefined) {
266
+ task.spawn(() => {
267
+ pcall(() => {
268
+ HttpService.RequestAsync({
269
+ Url: `${conn.serverUrl}/ack`,
270
+ Method: "POST",
271
+ Headers: {
272
+ "Content-Type": "application/json",
273
+ ...(conn.sessionToken ? { "Authorization": `Bearer ${conn.sessionToken}` } : {})
274
+ },
275
+ Body: HttpService.JSONEncode({
276
+ pluginSessionId,
277
+ requestId,
278
+ serverEpoch,
279
+ deliveryAttempt,
280
+ leaseToken
281
+ }),
282
+ });
283
+ });
212
284
  });
285
+ }
286
+
287
+ activeRequests.add(requestId);
288
+ task.spawn(() => {
289
+ const co = coroutine.running();
290
+ syncThreadRequests.set(co, requestId);
291
+ const [ok, result] = pcall(() => processRequest(request));
292
+ syncThreadRequests.delete(co);
293
+
294
+ const response = ok ? result : { error: tostring(result) };
295
+ activeRequests.delete(requestId);
296
+
297
+ if (completedRequestOrder.size() >= COMPLETED_REQUEST_LIMIT) {
298
+ const oldest = completedRequestOrder.remove(0);
299
+ if (oldest !== undefined) completedRequests.delete(oldest);
300
+ }
301
+ completedRequests.set(requestId, { response });
302
+ completedRequestOrder.push(requestId);
303
+
304
+ sendResponse(conn, requestId, response, serverEpoch, deliveryAttempt, leaseToken);
213
305
  });
214
306
  }
215
307
 
@@ -312,12 +404,34 @@ function sendReady(conn: Connection): void {
312
404
  const [parseOk, readyData] = pcall(
313
405
  () => HttpService.JSONDecode(readyResult.Body) as ReadyResponse,
314
406
  );
407
+ if (parseOk && readyData.sessionToken) {
408
+ conn.sessionToken = readyData.sessionToken;
409
+ }
315
410
  if (parseOk && readyData.assignedRole) {
316
411
  assignedRole = readyData.assignedRole;
317
412
  }
318
413
  lastReadyInstanceId = parseOk && typeIs(readyData.instanceId, "string") && readyData.instanceId !== ""
319
414
  ? readyData.instanceId
320
415
  : instanceId;
416
+
417
+ if (parseOk && readyData.serverEpoch && completedRequests.size() > 0) {
418
+ const receipts: { requestId: string; completedAt: number }[] = [];
419
+ for (const [id, _] of completedRequests) {
420
+ receipts.push({ requestId: id, completedAt: tick() * 1000 });
421
+ }
422
+ task.spawn(() => {
423
+ pcall(() => HttpService.RequestAsync({
424
+ Url: `${conn.serverUrl}/reconcile`,
425
+ Method: "POST",
426
+ Headers: { "Content-Type": "application/json" },
427
+ Body: HttpService.JSONEncode({
428
+ pluginSessionId,
429
+ serverEpoch: readyData.serverEpoch,
430
+ receipts,
431
+ }),
432
+ }));
433
+ });
434
+ }
321
435
  ServerUrlSettings.rememberServerUrl(conn.serverUrl);
322
436
  const connectedRole = assignedRole ?? detectRole();
323
437
  if (readyFailureLogKeys.has(readyLogKey)) {
@@ -332,16 +446,30 @@ function streamUrl(serverUrl: string): string {
332
446
  return `${websocketUrl}/stream?pluginSessionId=${pluginSessionId}`;
333
447
  }
334
448
 
335
- function sendStreamResponse(conn: Connection, requestId: string, response: unknown) {
449
+ function sendStreamResponse(conn: Connection, requestId: string, response: unknown, serverEpoch?: string, deliveryAttempt?: number, leaseToken?: string) {
336
450
  if (!conn.streamOpen || !conn.streamClient) return;
337
- pcall(() => conn.streamClient!.Send(HttpService.JSONEncode({ type: "response", requestId, response })));
451
+
452
+ const body: Record<string, unknown> = { type: "response", requestId, response };
453
+ if (serverEpoch !== undefined) body.serverEpoch = serverEpoch;
454
+ if (deliveryAttempt !== undefined) body.deliveryAttempt = deliveryAttempt;
455
+ if (leaseToken !== undefined) body.leaseToken = leaseToken;
456
+
457
+ const [ok, result] = pcall(() => conn.streamClient!.Send(HttpService.JSONEncode(body)));
458
+ if (!ok) warn(`[BloxForge] Failed to send stream response for ${requestId}: ${tostring(result)}`);
338
459
  }
339
460
 
340
461
  function startRequestStream(conn: Connection) {
341
462
  if (!conn.isActive || conn.streamClient) return;
342
463
  const [ok, stream] = pcall(() => HttpService.CreateWebStreamClient(
343
464
  Enum.WebStreamClientType.WebSocket,
344
- { Url: streamUrl(conn.serverUrl), Method: "GET", Headers: { "Content-Type": "application/json" } },
465
+ {
466
+ Url: streamUrl(conn.serverUrl),
467
+ Method: "GET",
468
+ Headers: {
469
+ "Content-Type": "application/json",
470
+ ...(conn.sessionToken ? { "Authorization": `Bearer ${conn.sessionToken}` } : {})
471
+ }
472
+ },
345
473
  ));
346
474
  if (!ok || !stream) return;
347
475
 
@@ -354,12 +482,10 @@ function startRequestStream(conn: Connection) {
354
482
  });
355
483
  stream.MessageReceived.Connect((message) => {
356
484
  if (conn.streamClient !== stream) return;
357
- const [parsed, frame] = pcall(() => HttpService.JSONDecode(message) as { type?: string; requestId?: string; request?: RequestPayload });
485
+ const [parsed, frame] = pcall(() => HttpService.JSONDecode(message) as { type?: string; requestId?: string; request?: RequestPayload; serverEpoch?: string; deliveryAttempt?: number; leaseToken?: string });
358
486
  if (!parsed || frame.type !== "request" || !frame.requestId || !frame.request) return;
359
- task.spawn(() => {
360
- const [handled, response] = pcall(() => processRequest(frame.request!));
361
- sendStreamResponse(conn, frame.requestId!, handled ? response : { error: tostring(response) });
362
- });
487
+
488
+ handleRequestOnce(conn, frame.requestId!, frame.request!, frame.serverEpoch, frame.deliveryAttempt, frame.leaseToken);
363
489
  });
364
490
  const close = () => {
365
491
  if (conn.streamClient !== stream) return;
@@ -382,7 +508,10 @@ function pollForRequests(connIndex: number) {
382
508
  return HttpService.RequestAsync({
383
509
  Url: `${conn.serverUrl}/poll?pluginSessionId=${pluginSessionId}`,
384
510
  Method: "GET",
385
- Headers: { "Content-Type": "application/json" },
511
+ Headers: {
512
+ "Content-Type": "application/json",
513
+ ...(conn.sessionToken ? { "Authorization": `Bearer ${conn.sessionToken}` } : {})
514
+ },
386
515
  });
387
516
  });
388
517
 
@@ -489,15 +618,14 @@ function pollForRequests(connIndex: number) {
489
618
  }
490
619
  }
491
620
 
621
+ if (data.cancellations) {
622
+ for (const c of data.cancellations) {
623
+ cancelledSyncRequests.add(c);
624
+ }
625
+ }
626
+
492
627
  if (data.request && mcpConnected) {
493
- task.spawn(() => {
494
- const [ok, response] = pcall(() => processRequest(data.request!));
495
- if (ok) {
496
- sendResponse(conn, data.requestId!, response);
497
- } else {
498
- sendResponse(conn, data.requestId!, { error: tostring(response) });
499
- }
500
- });
628
+ handleRequestOnce(conn, data.requestId!, data.request, data.serverEpoch, data.deliveryAttempt, data.leaseToken);
501
629
  }
502
630
  } else if (conn.isActive) {
503
631
  conn.consecutiveFailures++;
@@ -710,6 +838,7 @@ function checkForUpdates() {
710
838
 
711
839
  export = {
712
840
  getConnectionStatus,
841
+ isCancelledForThread,
713
842
  activatePlugin,
714
843
  deactivatePlugin,
715
844
  deactivateAll,
@@ -46,6 +46,30 @@ function nowSec(): number {
46
46
  return DateTime.now().UnixTimestampMillis / 1000;
47
47
  }
48
48
 
49
+ // Studio can expose binary-bearing Output messages that JSONEncode rejects.
50
+ // Preserve valid UTF-8 and make only malformed bytes visible and JSON-safe.
51
+ function escapeInvalidUtf8(message: string): string {
52
+ const [valid] = utf8.len(message);
53
+ if (typeIs(valid, "number")) return message;
54
+
55
+ const parts: string[] = [];
56
+ let cursor = 1;
57
+ while (cursor <= message.size()) {
58
+ const [suffixValid, invalidPosition] = utf8.len(message, cursor);
59
+ if (typeIs(suffixValid, "number")) {
60
+ parts.push(string.sub(message, cursor));
61
+ break;
62
+ }
63
+ if (!typeIs(invalidPosition, "number")) break;
64
+ if (invalidPosition > cursor) parts.push(string.sub(message, cursor, invalidPosition - 1));
65
+
66
+ const [invalidByte] = string.byte(message, invalidPosition);
67
+ parts.push(string.format("\\x%02X", invalidByte));
68
+ cursor = invalidPosition + 1;
69
+ }
70
+ return parts.join("");
71
+ }
72
+
49
73
  function dropOldestUntilFits(incomingBytes: number): void {
50
74
  while (
51
75
  entries.size() > 0 &&
@@ -58,9 +82,14 @@ function dropOldestUntilFits(incomingBytes: number): void {
58
82
  }
59
83
 
60
84
  function pushEntry(message: string, level: LogLevel, ts: number): void {
61
- const bytes = message.size();
85
+ const safeMessage = escapeInvalidUtf8(message);
86
+ const bytes = safeMessage.size();
87
+ if (bytes > MAX_BYTES) {
88
+ totalDropped += 1;
89
+ return;
90
+ }
62
91
  dropOldestUntilFits(bytes);
63
- entries.push({ seq: nextSeq, ts, level, message });
92
+ entries.push({ seq: nextSeq, ts, level, message: safeMessage });
64
93
  nextSeq += 1;
65
94
  totalBytes += bytes;
66
95
  }
@@ -2,7 +2,7 @@ import { Connection } from "../types";
2
2
 
3
3
  const CURRENT_VERSION = "__VERSION__";
4
4
  const PLUGIN_VARIANT = "__PLUGIN_VARIANT__";
5
- const PROTOCOL_VERSION = 1;
5
+ const PROTOCOL_VERSION = 3;
6
6
  const MAX_CONNECTIONS = 5;
7
7
  const BASE_PORT = 58741;
8
8
  let activeTabIndex = 0;
@@ -7,6 +7,7 @@
7
7
 
8
8
  import LuauExec from "../LuauExec";
9
9
  import JobRegistry from "../JobRegistry";
10
+ import Communication from "../Communication";
10
11
 
11
12
  // Install a sanctioned progress/cancel API once. Server-generated long-running
12
13
  // Luau (terrain/template/batch builders) can call _G.__mcp.progress(done,total,msg)
@@ -19,7 +20,7 @@ function installMcpGlobal(): void {
19
20
  progress: (done: number, total?: number, message?: string, stage?: string) => {
20
21
  JobRegistry.reportProgress(coroutine.running(), done, total, message, stage);
21
22
  },
22
- checkCancelled: () => JobRegistry.isCancelledForThread(coroutine.running()),
23
+ checkCancelled: () => JobRegistry.isCancelledForThread(coroutine.running()) || Communication.isCancelledForThread(coroutine.running()),
23
24
  };
24
25
  }
25
26
  installMcpGlobal();
@@ -3,6 +3,7 @@
3
3
  export interface Connection {
4
4
  port: number;
5
5
  serverUrl: string;
6
+ sessionToken?: string;
6
7
  isActive: boolean;
7
8
  pollInterval: number;
8
9
  lastPoll: number;
@@ -43,10 +44,14 @@ export interface PollResponse {
43
44
  protocolMismatch?: boolean;
44
45
  request?: RequestPayload;
45
46
  requestId?: string;
47
+ serverEpoch?: string;
48
+ deliveryAttempt?: number;
49
+ leaseToken?: string;
46
50
  // Server signals knownInstance=false when its in-memory instances map
47
51
  // doesn't contain our pluginSessionId (typically after an MCP process
48
52
  // restart). The plugin re-issues /ready when it sees this.
49
53
  knownInstance?: boolean;
54
+ cancellations?: string[];
50
55
  }
51
56
 
52
57
  export interface Job {
@@ -74,6 +79,8 @@ export interface ReadyResponse {
74
79
  serverProtocolVersion?: number;
75
80
  versionMismatch?: boolean;
76
81
  protocolMismatch?: boolean;
82
+ sessionToken?: string;
83
+ serverEpoch?: string;
77
84
  error?: string;
78
85
  message?: string;
79
86
  }