@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 +8 -10
- package/prebuilt/better-sqlite3/.gitkeep +0 -0
- package/prebuilt/better-sqlite3/linux-x64-node22/better_sqlite3.node +0 -0
- package/prebuilt/better-sqlite3/manifest.json +11 -0
- package/scripts/ensure-better-sqlite3.mjs +119 -0
- package/src/core/sqliteFallback.js +9 -1
- package/src/core/unityConnection.js +75 -11
- package/src/handlers/script/CodeIndexStatusToolHandler.js +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akiojin/unity-mcp-server",
|
|
3
|
-
"version": "2.
|
|
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
|
-
"
|
|
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
|
|
Binary file
|
|
@@ -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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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 {
|