@akiojin/unity-mcp-server 2.40.5 → 2.41.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "2.40.5",
3
+ "version": "2.41.0",
4
4
  "description": "MCP server and Unity Editor bridge — enables AI assistants to control Unity for AI-assisted workflows",
5
5
  "type": "module",
6
6
  "main": "src/core/server.js",
@@ -20,13 +20,15 @@
20
20
  "test:watch": "node --watch --test tests/unit/**/*.test.js",
21
21
  "test:watch:all": "node --watch --test tests/**/*.test.js",
22
22
  "test:performance": "node --test tests/performance/*.test.js",
23
- "test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
23
+ "test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js tests/unit/scripts/ensure-better-sqlite3.test.js",
24
24
  "test:ci:coverage": "c8 --reporter=lcov --reporter=text node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
25
25
  "test:ci:all": "c8 --reporter=lcov node --test tests/unit/**/*.test.js",
26
26
  "simulate:code-index": "node scripts/simulate-code-index-status.mjs",
27
27
  "test:verbose": "VERBOSE_TEST=true node --test tests/**/*.test.js",
28
28
  "prepare": "cd .. && husky || true",
29
- "prepublishOnly": "npm run test:ci",
29
+ "prebuild:better-sqlite3": "node scripts/prebuild-better-sqlite3.mjs",
30
+ "prebuilt:manifest": "node scripts/generate-prebuilt-manifest.mjs",
31
+ "prepublishOnly": "npm run test:ci && npm run prebuilt:manifest",
30
32
  "postinstall": "node scripts/ensure-better-sqlite3.mjs && chmod +x bin/unity-mcp-server || true",
31
33
  "test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js || exit 0",
32
34
  "test:unity": "node tests/run-unity-integration.mjs",
@@ -68,6 +70,7 @@
68
70
  "files": [
69
71
  "src/",
70
72
  "bin/",
73
+ "prebuilt/",
71
74
  "README.md",
72
75
  "LICENSE"
73
76
  ],
File without changes
@@ -0,0 +1,3 @@
1
+ {
2
+ "artifacts": []
3
+ }
@@ -10,6 +10,7 @@ export class UnityConnection extends EventEmitter {
10
10
  super();
11
11
  this.socket = null;
12
12
  this.connected = false;
13
+ this.connectPromise = null;
13
14
  this.reconnectAttempts = 0;
14
15
  this.reconnectTimer = null;
15
16
  this.commandId = 0;
@@ -27,7 +28,12 @@ export class UnityConnection extends EventEmitter {
27
28
  * @returns {Promise<void>}
28
29
  */
29
30
  async connect() {
30
- return new Promise((resolve, reject) => {
31
+ // Return the in-flight promise if we're already trying to connect.
32
+ if (this.connectPromise) {
33
+ return this.connectPromise;
34
+ }
35
+
36
+ this.connectPromise = new Promise((resolve, reject) => {
31
37
  if (this.connected) {
32
38
  resolve();
33
39
  return;
@@ -46,6 +52,13 @@ export class UnityConnection extends EventEmitter {
46
52
  this.socket = new net.Socket();
47
53
  let connectionTimeout = null;
48
54
  let resolved = false;
55
+ const settle = (fn, value) => {
56
+ if (resolved) return;
57
+ resolved = true;
58
+ clearConnectionTimeout();
59
+ this.connectPromise = null;
60
+ fn(value);
61
+ };
49
62
 
50
63
  // Helper to clean up the connection timeout
51
64
  const clearConnectionTimeout = () => {
@@ -60,10 +73,10 @@ export class UnityConnection extends EventEmitter {
60
73
  logger.info('Connected to Unity Editor');
61
74
  this.connected = true;
62
75
  this.reconnectAttempts = 0;
63
- resolved = true;
64
- clearConnectionTimeout();
76
+ this.connectPromise = null;
65
77
  this.emit('connected');
66
- resolve();
78
+ this._pumpQueue(); // flush any queued commands that arrived during reconnect
79
+ settle(resolve);
67
80
  });
68
81
 
69
82
  this.socket.on('data', data => {
@@ -72,11 +85,12 @@ export class UnityConnection extends EventEmitter {
72
85
 
73
86
  this.socket.on('error', error => {
74
87
  logger.error('Socket error:', error.message);
75
- this.emit('error', error);
88
+ if (this.listenerCount('error') > 0) {
89
+ this.emit('error', error);
90
+ }
76
91
 
77
92
  if (!this.connected && !resolved) {
78
- resolved = true;
79
- clearConnectionTimeout();
93
+ this.connectPromise = null;
80
94
  // Mark as disconnecting to prevent reconnection
81
95
  this.isDisconnecting = true;
82
96
  // Destroy the socket to clean up properly
@@ -89,6 +103,7 @@ export class UnityConnection extends EventEmitter {
89
103
  this.socket.on('close', () => {
90
104
  // Clear the connection timeout when socket closes
91
105
  clearConnectionTimeout();
106
+ this.connectPromise = null;
92
107
 
93
108
  // Check if we're already handling disconnection
94
109
  if (this.isDisconnecting || !this.socket) {
@@ -123,14 +138,15 @@ export class UnityConnection extends EventEmitter {
123
138
  // Set timeout for initial connection
124
139
  connectionTimeout = setTimeout(() => {
125
140
  if (!this.connected && !resolved && this.socket) {
126
- resolved = true;
127
141
  // Remove event listeners before destroying to prevent callbacks after timeout
128
142
  this.socket.removeAllListeners();
129
143
  this.socket.destroy();
130
- reject(new Error('Connection timeout'));
144
+ this.connectPromise = null;
145
+ settle(reject, new Error('Connection timeout'));
131
146
  }
132
147
  }, config.unity.commandTimeout);
133
148
  });
149
+ return this.connectPromise;
134
150
  }
135
151
 
136
152
  /**
@@ -166,6 +182,11 @@ export class UnityConnection extends EventEmitter {
166
182
  if (this.reconnectTimer) {
167
183
  return;
168
184
  }
185
+ // Avoid piling up reconnects if a connection attempt is already in progress
186
+ if (this.connectPromise) {
187
+ logger.info('Reconnect skipped; connection attempt already in progress');
188
+ return;
189
+ }
169
190
 
170
191
  const delay = Math.min(
171
192
  config.unity.reconnectDelay *
@@ -338,8 +359,10 @@ export class UnityConnection extends EventEmitter {
338
359
  logger.info(`[Unity] enqueue sendCommand: ${type}`, { connected: this.connected });
339
360
 
340
361
  if (!this.connected) {
341
- logger.error('[Unity] Cannot send command - not connected');
342
- throw new Error('Not connected to Unity');
362
+ logger.warn('[Unity] Not connected; waiting for reconnection before sending command');
363
+ await this.ensureConnected({
364
+ timeoutMs: config.unity.commandTimeout
365
+ });
343
366
  }
344
367
 
345
368
  // Create an external promise that will resolve when Unity responds
@@ -424,6 +447,43 @@ export class UnityConnection extends EventEmitter {
424
447
  return this.sendCommand('ping', {});
425
448
  }
426
449
 
450
+ /**
451
+ * Wait until the connection is available, attempting to reconnect if needed.
452
+ * @param {object} options
453
+ * @param {number} options.timeoutMs - Maximum time to wait for reconnection
454
+ */
455
+ async ensureConnected({ timeoutMs = config.unity.commandTimeout } = {}) {
456
+ const start = Date.now();
457
+ let lastError = null;
458
+
459
+ while (!this.connected && Date.now() - start < timeoutMs) {
460
+ try {
461
+ await this.connect();
462
+ } catch (error) {
463
+ lastError = error;
464
+ const msg = error?.message || '';
465
+ if (process.env.NODE_ENV === 'test' || process.env.CI === 'true') {
466
+ // In test/CI we bail out immediately to avoid long waits
467
+ throw error;
468
+ }
469
+ if (/test environment/i.test(msg)) {
470
+ throw error;
471
+ }
472
+ }
473
+
474
+ if (this.connected) return;
475
+ await sleep(Math.min(500, timeoutMs));
476
+ }
477
+
478
+ if (!this.connected) {
479
+ const error = new Error(
480
+ `Failed to reconnect to Unity within ${timeoutMs}ms${lastError ? `: ${lastError.message}` : ''}`
481
+ );
482
+ error.code = 'UNITY_RECONNECT_TIMEOUT';
483
+ throw error;
484
+ }
485
+ }
486
+
427
487
  /**
428
488
  * Checks if connected to Unity
429
489
  * @returns {boolean}
@@ -432,3 +492,7 @@ export class UnityConnection extends EventEmitter {
432
492
  return this.connected;
433
493
  }
434
494
  }
495
+
496
+ function sleep(ms) {
497
+ return new Promise(resolve => setTimeout(resolve, Math.max(0, ms || 0)));
498
+ }