@adiholick/nvstudio-mcp 2.1.8 → 2.2.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/README.md CHANGED
@@ -60,8 +60,7 @@ Jika Anda **tidak** menggunakan skrip installer otomatis di atas (misalnya hanya
60
60
 
61
61
  ## 🔌 Cara Menggunakan (Konfigurasi MCP Klien AI)
62
62
 
63
- Jika Anda menggunakan AI Client lain seperti **Claude Desktop**, **Windsurf**, atau ingin menambahkan konfigurasi secara manual:
64
-
63
+ ### Klien Berbasis NPM (Cursor, Claude Desktop, Windsurf)
65
64
  Tambahkan konfigurasi berikut ke file `mcp.json` Anda:
66
65
  ```json
67
66
  {
@@ -78,13 +77,30 @@ Tambahkan konfigurasi berikut ke file `mcp.json` Anda:
78
77
  ```
79
78
  *(AI Anda akan selalu menjalankan versi terbaru dari server Bridge langsung dari NPM!)*
80
79
 
80
+ ### Klien Berbasis Instalasi Lokal (Antigravity IDE)
81
+ Jika Anda menggunakan instalasi lokal via installer di `%USERPROFILE%\.nvstudio-mcp`, tambahkan ke `~/.gemini/config/mcp_config.json`:
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "nvstudio-mcp": {
86
+ "command": "node",
87
+ "args": [
88
+ "C:/Users/<Username>/.nvstudio-mcp/dist/index.js"
89
+ ]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
81
95
  ---
82
96
 
83
- ## 🖥️ Dashboard Visual & Fitur Pintar v2.0
97
+ ## 🖥️ Dashboard Visual & Fitur Unggulan v2.1
84
98
 
99
+ - **High-Performance Polling Stream & SSE:** Komunikasi antara Roblox Studio dan Bridge Server menggunakan arsitektur snapshot stream sub-detik yang sangat responsif, stabil, dan bebas latensi.
100
+ - **Clean Log Engine:** Log status pada Roblox Studio hanya diperbarui saat ada perubahan status koneksi sebenarnya (bebas dari log spam berulang).
85
101
  - **Visual Dashboard Real-time:** Buka [http://localhost:3055](http://localhost:3055) di browser Anda untuk melihat monitor antrean perintah, log aktivitas AI, status koneksi Studio, dan statistik eksekusi secara visual.
86
- - **Auto-Detect & Auto-Connect:** Plugin Roblox Studio otomatis menyambung ke server saat aktif dan kembali menunggu (*Waiting*) saat server mati tanpa interupsi manual.
87
- - **Anti-Zombie Port Handling:** Tidak perlu khawatir error `EADDRINUSE`. Jika port `3055` tertahan oleh proses zombie akibat restart editor/IDE, server akan otomatis membersihkan port tersebut secara aman.
102
+ - **Auto-Detect & Auto-Connect:** Plugin Roblox Studio otomatis menyambung ke server saat aktif dan kembali menunggu (*Waiting*) saat server mati tanpa perlu campur tangan manual.
103
+ - **Anti-Zombie Port Handling:** Port `3055` dibersihkan secara otomatis jika terjadi penutupan atau restart IDE secara paksa.
88
104
 
89
105
  ---
90
106
 
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ const task_queue_1 = require("./task-queue");
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const luau_validator_1 = require("./luau-validator");
15
- const HISTORY_DIR = path_1.default.join(process.cwd(), '.history');
15
+ const HISTORY_DIR = path_1.default.join(__dirname, '..', '.history');
16
16
  if (!fs_1.default.existsSync(HISTORY_DIR)) {
17
17
  fs_1.default.mkdirSync(HISTORY_DIR, { recursive: true });
18
18
  }
@@ -24,11 +24,12 @@ function backupScript(target, code) {
24
24
  }
25
25
  const isDaemon = process.argv.includes('--daemon') || process.argv.includes('-d');
26
26
  // 1. Impor dan jalankan server Express lokal di port 3055
27
- (0, roblox_bridge_1.startBridgeServer)(3055);
27
+ (0, roblox_bridge_1.startBridgeServer)(3055, isDaemon);
28
28
  // 2. Inisialisasi server MCP
29
+ const pkg = require('../package.json');
29
30
  const server = new index_js_1.Server({
30
31
  name: "nvstudio-mcp",
31
- version: "2.1.8",
32
+ version: pkg.version,
32
33
  }, {
33
34
  capabilities: {
34
35
  tools: {},
@@ -40,13 +41,13 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
40
41
  tools: [
41
42
  {
42
43
  name: "call_mcp_tool",
43
- description: "ALAT MUTLAK UNTUK ROBLOX. Command: patch_script_source, get_children, get_script_source, update_script_source, rollback_script, get_logs, create_instance, search_instance, generate_terrain, insert_asset, delete_instance, get_properties, script_grep, execute_luau, search_asset.",
44
+ description: "ALAT MUTLAK UNTUK ROBLOX. Command: patch_script_source, get_children, get_full_tree, get_script_source, update_script_source, rollback_script, get_logs, create_instance, search_instance, generate_terrain, insert_asset, delete_instance, get_properties, set_properties, script_grep, execute_luau, search_asset.",
44
45
  inputSchema: {
45
46
  type: "object",
46
47
  properties: {
47
48
  command: {
48
49
  type: "string",
49
- description: "Perintah yang dieksekusi (contoh: 'patch_script_source', 'get_script_source', 'update_script_source').",
50
+ description: "Perintah yang dieksekusi (contoh: 'set_properties', 'get_full_tree', 'patch_script_source').",
50
51
  },
51
52
  target: {
52
53
  type: "string",
@@ -54,10 +55,10 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
54
55
  },
55
56
  data: {
56
57
  type: "string",
57
- description: "Data tambahan (misal payload JSON untuk patch_script_source, atau source code penuh untuk update_script_source).",
58
+ description: "Data tambahan. Untuk set_properties: JSON string dari properti. Untuk delete_instance: kirim 'force' jika descendants > 10. Untuk patch/update: source code.",
58
59
  },
59
60
  },
60
- required: ["command", "target"],
61
+ required: ["command"],
61
62
  },
62
63
  },
63
64
  ],
@@ -70,14 +71,14 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
70
71
  }
71
72
  const args = request.params.arguments;
72
73
  const { command, target, data } = args;
73
- if (!command || !target) {
74
+ if (!command) {
74
75
  return {
75
76
  content: [
76
77
  {
77
78
  type: "text",
78
79
  text: JSON.stringify({
79
80
  status: "error",
80
- error: "Parameter 'command' dan 'target' wajib diisi.",
81
+ error: "Parameter 'command' wajib diisi.",
81
82
  }),
82
83
  },
83
84
  ],
@@ -99,7 +100,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
99
100
  isError: true
100
101
  };
101
102
  }
102
- backupScript(target, String(data));
103
103
  }
104
104
  // Intercept command yang dieksekusi di server lokal (Node.js) alih-alih di Studio
105
105
  if (command === "search_asset" && data) {
@@ -137,9 +137,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
137
137
  }
138
138
  // 8. Teruskan ke antrean tugas
139
139
  try {
140
- if (command === "update_script_source" && data) {
141
- backupScript(target, String(data));
142
- }
143
140
  let result;
144
141
  if (roblox_bridge_1.isBridgeHosting) {
145
142
  // Instance ini sendiri yang meng-host bridge server
@@ -166,13 +163,15 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
166
163
  }
167
164
  result = bridgeJson.result;
168
165
  }
166
+ else {
167
+ throw new Error(`Bridge server returned HTTP ${bridgeRes.status}`);
168
+ }
169
169
  }
170
170
  catch (err) {
171
171
  if (forwardedToBridge) {
172
172
  throw err;
173
173
  }
174
- // Fallback in-process jika HTTP gagal dihubungi
175
- result = await (0, task_queue_1.addTaskToQueue)(command, target, data);
174
+ throw new Error("Gagal menghubungi server Bridge. Pastikan server daemon berjalan di port 3055.");
176
175
  }
177
176
  }
178
177
  return {
@@ -6,9 +6,6 @@ function validateLuauSyntax(source) {
6
6
  // Stack for blocks: function, do, if, for, while, repeat
7
7
  // We'll keep it simple: just count block openers vs block closers (end, until)
8
8
  // Note: This is a heuristic pre-commit check. It doesn't replace a full AST parser,
9
- // but catches 90% of AI hallucination syntax errors (missing 'end', etc.)
10
- let blockDepth = 0;
11
- let repeatDepth = 0;
12
9
  // Bracket stacks
13
10
  let parenDepth = 0; // ()
14
11
  let braceDepth = 0; // {}
@@ -84,6 +81,10 @@ function validateLuauSyntax(source) {
84
81
  j = k + 1;
85
82
  continue;
86
83
  }
84
+ // If it ended at end of line, we can't reliably detect it. Let's just avoid bracket counting
85
+ if (line[k] === undefined) {
86
+ break;
87
+ }
87
88
  }
88
89
  }
89
90
  if (inString) {
@@ -116,61 +117,34 @@ function validateLuauSyntax(source) {
116
117
  }
117
118
  // At this point, we are outside strings and comments.
118
119
  // Check brackets
119
- if (char === '(')
120
- parenDepth++;
121
- else if (char === ')') {
122
- parenDepth--;
123
- if (parenDepth < 0)
124
- return { valid: false, error: 'Kelebihan tutup kurung ")"', line: i + 1 };
125
- }
126
- else if (char === '{')
127
- braceDepth++;
128
- else if (char === '}') {
129
- braceDepth--;
130
- if (braceDepth < 0)
131
- return { valid: false, error: 'Kelebihan tutup kurawal "}"', line: i + 1 };
132
- }
133
- else if (char === '[')
134
- bracketDepth++;
135
- else if (char === ']') {
136
- bracketDepth--;
137
- if (bracketDepth < 0)
138
- return { valid: false, error: 'Kelebihan tutup siku "]"', line: i + 1 };
120
+ if (!inString && !inMultilineString && !inMultilineComment) {
121
+ if (char === '(')
122
+ parenDepth++;
123
+ else if (char === ')') {
124
+ parenDepth--;
125
+ if (parenDepth < 0)
126
+ return { valid: false, error: 'Kelebihan tutup kurung ")"', line: i + 1 };
127
+ }
128
+ else if (char === '{')
129
+ braceDepth++;
130
+ else if (char === '}') {
131
+ braceDepth--;
132
+ if (braceDepth < 0)
133
+ return { valid: false, error: 'Kelebihan tutup kurawal "}"', line: i + 1 };
134
+ }
135
+ else if (char === '[')
136
+ bracketDepth++;
137
+ else if (char === ']') {
138
+ bracketDepth--;
139
+ if (bracketDepth < 0)
140
+ return { valid: false, error: 'Kelebihan tutup siku "]"', line: i + 1 };
141
+ }
139
142
  }
140
143
  j++;
141
144
  }
142
145
  if (inString) {
143
146
  return { valid: false, error: 'String tidak ditutup (unclosed string literal)', line: i + 1 };
144
147
  }
145
- // Tokenize line to check blocks (very simplified heuristic)
146
- // We only check for keywords bounded by word boundaries
147
- if (!inMultilineString && !inMultilineComment) {
148
- // Remove string literals and comments for naive keyword counting
149
- let cleanLine = line.replace(/(--.*)/, ''); // remove single line comments
150
- cleanLine = cleanLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, ''); // remove inline strings
151
- // Avoid false positives like `local func = "do"` by looking at word boundaries
152
- // This is a naive regex matching.
153
- // In Luau: 'do', 'if', 'function', 'for', 'while' need an 'end'.
154
- // 'repeat' needs 'until'.
155
- const words = cleanLine.split(/[^a-zA-Z0-9_]+/).filter(w => w.length > 0);
156
- for (let w = 0; w < words.length; w++) {
157
- const word = words[w];
158
- if (word === 'if' || word === 'function' || word === 'for' || word === 'while' || word === 'do') {
159
- // 'elseif' does not create a new block.
160
- // 'if' does.
161
- blockDepth++;
162
- }
163
- else if (word === 'end') {
164
- blockDepth--;
165
- }
166
- else if (word === 'repeat') {
167
- repeatDepth++;
168
- }
169
- else if (word === 'until') {
170
- repeatDepth--;
171
- }
172
- }
173
- }
174
148
  }
175
149
  if (inMultilineString)
176
150
  return { valid: false, error: 'Multiline string tidak ditutup (unclosed [[ ]])' };
@@ -182,10 +156,5 @@ function validateLuauSyntax(source) {
182
156
  return { valid: false, error: 'Kekurangan tutup kurawal "}"' };
183
157
  if (bracketDepth > 0)
184
158
  return { valid: false, error: 'Kekurangan tutup siku "]"' };
185
- // We can't strictly enforce blockDepth == 0 because our heuristic is very naive
186
- // and might be thrown off by complex inline patterns, but it catches massive missing 'end's.
187
- // So we'll return a warning instead of blocking if blockDepth != 0,
188
- // or we can just let it pass to Studio which will output the real error.
189
- // For now, let's just use it as a basic check.
190
159
  return { valid: true };
191
160
  }
@@ -12,6 +12,7 @@ const socket_io_1 = require("socket.io");
12
12
  const path_1 = __importDefault(require("path"));
13
13
  const child_process_1 = require("child_process");
14
14
  const task_queue_1 = require("./task-queue");
15
+ const { version: SERVER_VERSION } = require('../package.json');
15
16
  exports.isBridgeHosting = false;
16
17
  // Global process error handlers agar server bridge tidak mati mendadak
17
18
  process.on('uncaughtException', (err) => {
@@ -27,17 +28,32 @@ let errorCount = 0;
27
28
  const sessionStart = new Date().toISOString();
28
29
  // Per-studio pending events queue: events yang belum diambil oleh Studio
29
30
  const studioEventQueues = new Map();
31
+ const studioLastSeen = new Map();
32
+ setInterval(() => {
33
+ const now = Date.now();
34
+ for (const [sid, lastSeen] of studioLastSeen.entries()) {
35
+ if (now - lastSeen > 60000) {
36
+ studioEventQueues.delete(sid);
37
+ studioLastSeen.delete(sid);
38
+ }
39
+ }
40
+ }, 30000);
30
41
  function enqueueForStudio(studioId, event) {
31
42
  if (studioId) {
32
43
  // Event untuk studio tertentu
33
44
  if (!studioEventQueues.has(studioId))
34
45
  studioEventQueues.set(studioId, []);
35
- studioEventQueues.get(studioId).push(event);
46
+ const q = studioEventQueues.get(studioId);
47
+ q.push(event);
48
+ if (q.length > 100)
49
+ q.shift();
36
50
  }
37
51
  else {
38
52
  // Broadcast ke semua studio yang terdaftar
39
- for (const [sid] of studioEventQueues) {
40
- studioEventQueues.get(sid).push(event);
53
+ for (const [sid, q] of studioEventQueues.entries()) {
54
+ q.push(event);
55
+ if (q.length > 100)
56
+ q.shift();
41
57
  }
42
58
  }
43
59
  }
@@ -77,11 +93,9 @@ function broadcastStatus() {
77
93
  const statusEvent = {
78
94
  mcpConnected,
79
95
  studioCount: Math.max(ssePeers.size, studioEventQueues.size),
80
- serverVersion: '2.1.8',
96
+ serverVersion: SERVER_VERSION,
81
97
  };
82
98
  broadcastEvent('status', statusEvent);
83
- // Juga enqueue untuk Studio yang belum polling saat ini
84
- enqueueForStudio(null, { kind: 'status', ...statusEvent });
85
99
  }
86
100
  /**
87
101
  * Push task ke semua Studio peer (broadcast ke semua Studio yang terbuka).
@@ -95,14 +109,13 @@ function broadcastTask(task) {
95
109
  data: task.data ?? null,
96
110
  remainingMs: 29000,
97
111
  };
98
- broadcastEvent('request', taskEvent);
99
- // Juga enqueue untuk Studio yang sedang tidak polling
100
112
  enqueueForStudio(null, { kind: 'request', ...taskEvent });
101
113
  }
102
114
  // ── Logging ───────────────────────────────────────────────────────────────────
115
+ let logCounter = 0;
103
116
  function addLog(io, type, message) {
104
117
  const log = {
105
- id: Date.now().toString(),
118
+ id: `${Date.now()}-${++logCounter}`,
106
119
  time: new Date().toLocaleTimeString('id-ID'),
107
120
  type,
108
121
  message,
@@ -123,6 +136,9 @@ function killZombieOnPort(port) {
123
136
  const lines = output.trim().split('\n');
124
137
  for (const line of lines) {
125
138
  const parts = line.trim().split(/\s+/);
139
+ const localAddress = parts[1];
140
+ if (!localAddress || !localAddress.endsWith(`:${port}`))
141
+ continue;
126
142
  const pid = parts[parts.length - 1];
127
143
  if (pid && pid !== '0' && pid !== String(process.pid)) {
128
144
  try {
@@ -152,7 +168,7 @@ function killZombieOnPort(port) {
152
168
  return false;
153
169
  }
154
170
  }
155
- function startBridgeServer(port = 3055) {
171
+ function startBridgeServer(port, isDaemon = false) {
156
172
  const app = (0, express_1.default)();
157
173
  const httpServer = (0, http_1.createServer)(app);
158
174
  const io = new socket_io_1.Server(httpServer, { cors: { origin: '*' } });
@@ -178,10 +194,16 @@ function startBridgeServer(port = 3055) {
178
194
  });
179
195
  });
180
196
  // ── /api/ping ─────────────────────────────────────────────────────────────
181
- // Dipertahankan untuk backward-compat (installer check, health check).
182
197
  app.get('/api/ping', (req, res) => {
198
+ const uptime = Math.floor((Date.now() - new Date(sessionStart).getTime()) / 1000);
183
199
  const activeStudios = Math.max(ssePeers.size, studioEventQueues.size);
184
- res.json({ status: 'ok', studioCount: activeStudios, mcpConnected, timestamp: Date.now() });
200
+ res.json({
201
+ status: 'ok',
202
+ version: SERVER_VERSION,
203
+ uptimeSeconds: uptime,
204
+ totalTasksProcessed: totalTasks,
205
+ activeStudios: activeStudios
206
+ });
185
207
  });
186
208
  // ── /api/stream (Snapshot endpoint untuk Roblox GetAsync) ───────────────
187
209
  // Roblox HttpService:GetAsync() TIDAK bisa streaming — ia block sampai
@@ -190,12 +212,13 @@ function startBridgeServer(port = 3055) {
190
212
  // Plugin mengulang GET ini setiap ~0.5 detik (pseudo-polling via snapshot).
191
213
  app.get('/api/stream', (req, res) => {
192
214
  const studioId = req.query.studioId || `anon-${Date.now()}`;
215
+ studioLastSeen.set(studioId, Date.now());
193
216
  // Daftarkan studio jika belum ada
194
217
  if (!studioEventQueues.has(studioId)) {
195
218
  studioEventQueues.set(studioId, []);
196
219
  io.emit('studio-status', { connected: true, studioCount: studioEventQueues.size, studioId });
197
220
  addLog(io, 'system', `Studio terdaftar. (ID: ${studioId.substring(0, 8)}…, Total: ${studioEventQueues.size})`);
198
- if (!dashboardOpened) {
221
+ if (!isDaemon && !dashboardOpened) {
199
222
  dashboardOpened = true;
200
223
  const url = `http://localhost:${port}`;
201
224
  const startCmd = process.platform === 'win32' ? `start "" "${url}"` : process.platform === 'darwin' ? `open "${url}"` : `xdg-open "${url}"`;
@@ -210,7 +233,7 @@ function startBridgeServer(port = 3055) {
210
233
  studioEventQueues.set(studioId, []); // kosongkan antrian
211
234
  // Selalu sertakan status terkini
212
235
  const events = [
213
- { kind: 'status', mcpConnected, studioCount: studioEventQueues.size, serverVersion: '2.1.8' },
236
+ { kind: 'status', mcpConnected, studioCount: studioEventQueues.size, serverVersion: SERVER_VERSION },
214
237
  ];
215
238
  // Tambahkan events tertunda (misalnya task requests)
216
239
  for (const ev of queue) {
@@ -218,20 +241,6 @@ function startBridgeServer(port = 3055) {
218
241
  if (ev.kind !== 'status')
219
242
  events.push(ev);
220
243
  }
221
- // Jika ada task di queue global, sertakan juga
222
- if (task_queue_1.taskQueue.length > 0) {
223
- const task = task_queue_1.taskQueue.shift();
224
- totalTasks++;
225
- addLog(io, 'task', `Mengirim perintah '${task.command}' ke Studio (ID: ${studioId.substring(0, 8)}…).`);
226
- events.push({
227
- kind: 'request',
228
- requestId: task.id,
229
- command: task.command,
230
- target: task.target,
231
- data: task.data ?? null,
232
- remainingMs: 29000,
233
- });
234
- }
235
244
  // Format sebagai teks SSE (setiap event = satu baris "data: ...\n\n")
236
245
  res.setHeader('Content-Type', 'text/plain');
237
246
  res.setHeader('Cache-Control', 'no-cache, no-store');
@@ -248,18 +257,26 @@ function startBridgeServer(port = 3055) {
248
257
  res.setHeader('Connection', 'keep-alive');
249
258
  res.setHeader('X-Accel-Buffering', 'no');
250
259
  res.flushHeaders();
260
+ let silenceTimer;
251
261
  const heartbeatTimer = setInterval(() => {
252
262
  try {
253
- res.write(`data: ${JSON.stringify({ kind: 'heartbeat', timestamp: Date.now() })}\n\n`);
263
+ const payload = JSON.stringify({
264
+ kind: 'heartbeat',
265
+ timestamp: Date.now(),
266
+ mcpConnected,
267
+ serverVersion: SERVER_VERSION,
268
+ });
269
+ res.write(`data: ${payload}\n\n`);
270
+ silenceTimer.refresh();
254
271
  }
255
272
  catch {
256
273
  cleanup();
257
274
  }
258
275
  }, 5000);
259
- const silenceTimer = setTimeout(() => cleanup(), 60000);
276
+ silenceTimer = setTimeout(() => cleanup(), 60000);
260
277
  const peer = { studioId, res, connectedAt: Date.now(), heartbeatTimer, silenceTimer };
261
278
  ssePeers.set(studioId, peer);
262
- sendEvent(peer, 'status', { mcpConnected, studioCount: studioEventQueues.size, serverVersion: '2.1.8' });
279
+ sendEvent(peer, 'status', { mcpConnected, studioCount: studioEventQueues.size, serverVersion: SERVER_VERSION });
263
280
  function cleanup() {
264
281
  clearInterval(heartbeatTimer);
265
282
  clearTimeout(silenceTimer);
@@ -304,11 +321,10 @@ function startBridgeServer(port = 3055) {
304
321
  // ── Notifikasi task baru ke Dashboard ────────────────────────────────────
305
322
  // Pengiriman task ke Studio dilakukan via /api/stream (snapshot polling).
306
323
  // Di sini kita hanya log ke dashboard browser.
307
- task_queue_1.taskEmitter.on('new_task', () => {
308
- if (task_queue_1.taskQueue.length === 0)
309
- return;
310
- const task = task_queue_1.taskQueue[0]; // preview saja, tidak shift
311
- addLog(io, 'task', `Task baru antri: '${task.command}'${task.target ? ` → ${task.target}` : ''}. Menunggu Studio polling...`);
324
+ task_queue_1.taskEmitter.on('new_task', (task) => {
325
+ totalTasks++;
326
+ addLog(io, 'task', `Task dikirim ke Studio: '${task.command}'${task.target ? ` → ${task.target}` : ''}`);
327
+ broadcastTask(task);
312
328
  });
313
329
  // ── /api/tasks/enqueue (dari proses MCP eksternal / bridge forwarding) ────
314
330
  app.post('/api/tasks/enqueue', async (req, res) => {
@@ -324,32 +340,30 @@ function startBridgeServer(port = 3055) {
324
340
  // ── /api/tasks (legacy long-poll — dipertahankan untuk backward-compat) ───
325
341
  // Studio lama yang belum update plugin masih bisa bekerja via polling.
326
342
  const waitingPollers = [];
327
- task_queue_1.taskEmitter.on('new_task_legacy', () => {
328
- if (task_queue_1.taskQueue.length > 0 && waitingPollers.length > 0) {
343
+ task_queue_1.taskEmitter.on('new_task', (task) => {
344
+ if (waitingPollers.length > 0) {
329
345
  const pollRes = waitingPollers.shift();
330
- const task = task_queue_1.taskQueue.shift();
331
- totalTasks++;
332
- addLog(io, 'task', `[Legacy] Mengirim perintah '${task?.command}' ke Studio.`);
346
+ const index = task_queue_1.taskQueue.findIndex(t => t.id === task.id);
347
+ if (index !== -1)
348
+ task_queue_1.taskQueue.splice(index, 1);
349
+ for (const queue of studioEventQueues.values()) {
350
+ const qIndex = queue.findIndex((e) => e.requestId === task.id);
351
+ if (qIndex !== -1)
352
+ queue.splice(qIndex, 1);
353
+ }
354
+ addLog(io, 'task', `[Legacy] Mengirim perintah '${task.command}' ke Studio.`);
333
355
  pollRes?.json(task);
334
356
  }
335
357
  });
336
358
  app.get('/api/tasks', (req, res) => {
337
359
  if (task_queue_1.taskQueue.length > 0) {
338
360
  const task = task_queue_1.taskQueue.shift();
339
- totalTasks++;
340
- addLog(io, 'task', `[Legacy] Mengirim perintah '${task?.command}' ke Studio.`);
341
- return res.json(task);
361
+ addLog(io, 'task', `[Legacy] Mengirim perintah tertunda '${task?.command}' ke Studio.`);
362
+ res.json(task);
363
+ return;
342
364
  }
343
- const timeoutId = setTimeout(() => {
344
- const index = waitingPollers.indexOf(res);
345
- if (index !== -1) {
346
- waitingPollers.splice(index, 1);
347
- res.json({ id: null });
348
- }
349
- }, 20000);
350
365
  waitingPollers.push(res);
351
366
  req.on('close', () => {
352
- clearTimeout(timeoutId);
353
367
  const index = waitingPollers.indexOf(res);
354
368
  if (index !== -1)
355
369
  waitingPollers.splice(index, 1);
@@ -26,7 +26,18 @@ function addTaskToQueue(command, target, data) {
26
26
  }, 30000);
27
27
  exports.pendingTasks.set(id, { resolve, reject, timer });
28
28
  exports.taskQueue.push(task);
29
- exports.taskEmitter.emit('new_task');
29
+ if (exports.taskQueue.length > 50) {
30
+ const droppedTask = exports.taskQueue.shift();
31
+ if (droppedTask) {
32
+ const pending = exports.pendingTasks.get(droppedTask.id);
33
+ if (pending) {
34
+ clearTimeout(pending.timer);
35
+ pending.reject(new Error("Timeout: Antrean Studio penuh, task dibuang."));
36
+ exports.pendingTasks.delete(droppedTask.id);
37
+ }
38
+ }
39
+ }
40
+ exports.taskEmitter.emit('new_task', task);
30
41
  });
31
42
  }
32
43
  /**
@@ -39,6 +50,9 @@ function resolvePendingTask(id, status, result, error) {
39
50
  }
40
51
  clearTimeout(pending.timer);
41
52
  exports.pendingTasks.delete(id);
53
+ const index = exports.taskQueue.findIndex(t => t.id === id);
54
+ if (index !== -1)
55
+ exports.taskQueue.splice(index, 1);
42
56
  if (status === "success") {
43
57
  pending.resolve(result);
44
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adiholick/nvstudio-mcp",
3
- "version": "2.1.8",
3
+ "version": "2.2.0",
4
4
  "description": "Local Bridge Server MCP for Roblox Studio",
5
5
  "main": "dist/index.js",
6
6
  "bin": {