@akiojin/unity-mcp-server 2.40.6 → 2.41.1

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.6",
3
+ "version": "2.41.1",
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,8 @@
68
70
  "files": [
69
71
  "src/",
70
72
  "bin/",
73
+ "scripts/ensure-better-sqlite3.mjs",
74
+ "prebuilt/",
71
75
  "README.md",
72
76
  "LICENSE"
73
77
  ],
@@ -78,11 +82,6 @@
78
82
  "devDependencies": {
79
83
  "@commitlint/cli": "^18.6.1",
80
84
  "@commitlint/config-conventional": "^18.6.3",
81
- "@semantic-release/changelog": "^6.0.3",
82
- "@semantic-release/commit-analyzer": "^11.1.0",
83
- "@semantic-release/exec": "^6.0.3",
84
- "@semantic-release/git": "^10.0.1",
85
- "@semantic-release/release-notes-generator": "^12.1.0",
86
85
  "c8": "^10.1.3",
87
86
  "eslint": "^8.57.1",
88
87
  "eslint-config-standard": "^17.1.0",
@@ -92,7 +91,6 @@
92
91
  "husky": "^9.1.7",
93
92
  "markdownlint-cli": "^0.43.0",
94
93
  "nodemon": "^3.1.7",
95
- "prettier": "^3.4.2",
96
- "semantic-release": "^22.0.12"
94
+ "prettier": "^3.4.2"
97
95
  }
98
96
  }
File without changes
@@ -0,0 +1,11 @@
1
+ {
2
+ "artifacts": [
3
+ {
4
+ "platformKey": "linux-x64-node22",
5
+ "checksum": null,
6
+ "node": null,
7
+ "generatedAt": null,
8
+ "size": 2066648
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // Ensure better-sqlite3 native binding exists. Prefers bundled prebuilt binaries, otherwise
3
+ // optionally attempts a native rebuild (opt-in to avoid first-time npx timeouts).
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { spawnSync } from 'child_process';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
10
+ const PKG_ROOT = path.resolve(path.dirname(SCRIPT_PATH), '..');
11
+
12
+ const DEFAULT_BINDING_PATH = process.env.UNITY_MCP_BINDING_PATH
13
+ ? path.resolve(process.env.UNITY_MCP_BINDING_PATH)
14
+ : path.join(
15
+ PKG_ROOT,
16
+ 'node_modules',
17
+ 'better-sqlite3',
18
+ 'build',
19
+ 'Release',
20
+ 'better_sqlite3.node'
21
+ );
22
+
23
+ const DEFAULT_PREBUILT_ROOT = process.env.UNITY_MCP_PREBUILT_DIR
24
+ ? path.resolve(process.env.UNITY_MCP_PREBUILT_DIR)
25
+ : path.join(PKG_ROOT, 'prebuilt', 'better-sqlite3');
26
+
27
+ export function resolvePlatformKey(
28
+ nodeVersion = process.versions.node,
29
+ platform = process.platform,
30
+ arch = process.arch
31
+ ) {
32
+ const major = String(nodeVersion).split('.')[0];
33
+ return `${platform}-${arch}-node${major}`;
34
+ }
35
+
36
+ function copyPrebuiltBinding(prebuiltDir, bindingTarget, log) {
37
+ const platformKey = resolvePlatformKey();
38
+ const source = path.join(prebuiltDir, platformKey, 'better_sqlite3.node');
39
+ if (!fs.existsSync(source)) return false;
40
+ fs.mkdirSync(path.dirname(bindingTarget), { recursive: true });
41
+ fs.copyFileSync(source, bindingTarget);
42
+ if (log) log(`[postinstall] Installed better-sqlite3 prebuilt for ${platformKey}`);
43
+ return true;
44
+ }
45
+
46
+ function rebuildNative(bindingTarget, pkgRoot, log) {
47
+ log(
48
+ '[postinstall] No prebuilt available. Attempting native rebuild (npm rebuild better-sqlite3 --build-from-source)'
49
+ );
50
+ const result = spawnSync('npm', ['rebuild', 'better-sqlite3', '--build-from-source'], {
51
+ stdio: 'inherit',
52
+ shell: false,
53
+ cwd: pkgRoot
54
+ });
55
+ if (result.status !== 0) {
56
+ throw new Error(`better-sqlite3 rebuild failed with code ${result.status ?? 'null'}`);
57
+ }
58
+ if (!fs.existsSync(bindingTarget)) {
59
+ throw new Error('better-sqlite3 rebuild completed but binding was still not found');
60
+ }
61
+ return true;
62
+ }
63
+
64
+ export function ensureBetterSqlite3(options = {}) {
65
+ const {
66
+ bindingPath = DEFAULT_BINDING_PATH,
67
+ prebuiltRoot = DEFAULT_PREBUILT_ROOT,
68
+ pkgRoot = PKG_ROOT,
69
+ skipNative = process.env.UNITY_MCP_SKIP_NATIVE_BUILD !== '0',
70
+ forceNative = process.env.UNITY_MCP_FORCE_NATIVE === '1',
71
+ skipLegacyFlag = Boolean(process.env.SKIP_SQLITE_REBUILD),
72
+ log = console.log,
73
+ warn = console.warn
74
+ } = options;
75
+
76
+ if (skipLegacyFlag && !forceNative) {
77
+ warn('[postinstall] SKIP_SQLITE_REBUILD set, skipping better-sqlite3 check');
78
+ return { status: 'skipped' };
79
+ }
80
+
81
+ if (!forceNative && copyPrebuiltBinding(prebuiltRoot, bindingPath, log)) {
82
+ return { status: 'copied' };
83
+ }
84
+
85
+ if (skipNative && !forceNative) {
86
+ warn(
87
+ '[postinstall] UNITY_MCP_SKIP_NATIVE_BUILD=1 -> skipping native rebuild; sql.js fallback will be used'
88
+ );
89
+ return { status: 'skipped' };
90
+ }
91
+
92
+ if (forceNative) {
93
+ log('[postinstall] UNITY_MCP_FORCE_NATIVE=1 -> forcing better-sqlite3 rebuild');
94
+ } else if (fs.existsSync(bindingPath)) {
95
+ // Binding already exists and native rebuild not forced.
96
+ return { status: 'existing' };
97
+ }
98
+
99
+ rebuildNative(bindingPath, pkgRoot, log);
100
+ return { status: 'rebuilt' };
101
+ }
102
+
103
+ function isCliExecution() {
104
+ const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
105
+ return invokedPath === SCRIPT_PATH;
106
+ }
107
+
108
+ async function main() {
109
+ try {
110
+ await ensureBetterSqlite3();
111
+ } catch (err) {
112
+ console.warn(`[postinstall] Warning: ${err.message}`);
113
+ // Do not hard fail install; runtime may still use sql.js fallback in codeIndex
114
+ }
115
+ }
116
+
117
+ if (isCliExecution()) {
118
+ main();
119
+ }
@@ -1,10 +1,18 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { createRequire } from 'module';
3
5
  import initSqlJs from 'sql.js';
4
6
 
5
7
  // Create a lightweight better-sqlite3 compatible surface using sql.js (WASM)
6
8
  export async function createSqliteFallback(dbPath) {
7
- const wasmPath = path.resolve('node_modules/sql.js/dist/sql-wasm.wasm');
9
+ const require = createRequire(import.meta.url);
10
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
11
+ const pkgRoot = path.resolve(moduleDir, '..', '..');
12
+ const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm', {
13
+ // Resolve from package-local node_modules first, then workspace root
14
+ paths: [path.join(pkgRoot, 'node_modules'), path.join(pkgRoot, '..', 'node_modules')]
15
+ });
8
16
  const SQL = await initSqlJs({ locateFile: () => wasmPath });
9
17
 
10
18
  const loadDb = () => {
@@ -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
+ }
@@ -32,6 +32,17 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
32
32
 
33
33
  // Check whether the persistent index already exists or is being built in the background.
34
34
  const ready = await this.codeIndex.isReady();
35
+ if (this.codeIndex.disabled) {
36
+ return {
37
+ success: false,
38
+ error: 'code_index_unavailable',
39
+ message:
40
+ this.codeIndex.disableReason ||
41
+ 'Code index is disabled because the SQLite driver could not be loaded. The server will continue without the symbol index.',
42
+ remediation:
43
+ 'Install native build tools (python3, make, g++) in this environment and run "npm rebuild better-sqlite3 --build-from-source", then restart the server.'
44
+ };
45
+ }
35
46
  const buildInProgress = latestBuildJob?.status === 'running';
36
47
  if (!ready && !buildInProgress) {
37
48
  return {