@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.
|
|
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
|
-
"
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
64
|
-
clearConnectionTimeout();
|
|
76
|
+
this.connectPromise = null;
|
|
65
77
|
this.emit('connected');
|
|
66
|
-
|
|
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.
|
|
88
|
+
if (this.listenerCount('error') > 0) {
|
|
89
|
+
this.emit('error', error);
|
|
90
|
+
}
|
|
76
91
|
|
|
77
92
|
if (!this.connected && !resolved) {
|
|
78
|
-
|
|
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
|
-
|
|
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.
|
|
342
|
-
|
|
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
|
+
}
|