@akiojin/unity-mcp-server 4.0.0 → 4.1.2
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 +1 -1
- package/src/core/config.js +27 -0
- package/src/core/projectInfo.js +17 -2
- package/src/core/unityConnection.js +72 -3
package/package.json
CHANGED
package/src/core/config.js
CHANGED
|
@@ -115,6 +115,12 @@ const baseConfig = {
|
|
|
115
115
|
description: 'MCP server for Unity Editor integration'
|
|
116
116
|
},
|
|
117
117
|
|
|
118
|
+
// Compatibility / safety checks
|
|
119
|
+
compat: {
|
|
120
|
+
// warn|error|off
|
|
121
|
+
versionMismatch: 'warn'
|
|
122
|
+
},
|
|
123
|
+
|
|
118
124
|
// Logging settings
|
|
119
125
|
logging: {
|
|
120
126
|
level: 'info',
|
|
@@ -171,6 +177,7 @@ function loadEnvConfig() {
|
|
|
171
177
|
const projectRoot = envString('UNITY_PROJECT_ROOT');
|
|
172
178
|
|
|
173
179
|
const logLevel = envString('UNITY_MCP_LOG_LEVEL');
|
|
180
|
+
const versionMismatch = envString('UNITY_MCP_VERSION_MISMATCH');
|
|
174
181
|
|
|
175
182
|
const httpEnabled = parseBoolEnv(process.env.UNITY_MCP_HTTP_ENABLED);
|
|
176
183
|
const httpPort = parseIntEnv(process.env.UNITY_MCP_HTTP_PORT);
|
|
@@ -197,6 +204,10 @@ function loadEnvConfig() {
|
|
|
197
204
|
out.logging = { level: logLevel };
|
|
198
205
|
}
|
|
199
206
|
|
|
207
|
+
if (versionMismatch) {
|
|
208
|
+
out.compat = { versionMismatch };
|
|
209
|
+
}
|
|
210
|
+
|
|
200
211
|
if (httpEnabled !== undefined || httpPort !== undefined) {
|
|
201
212
|
out.http = {};
|
|
202
213
|
if (httpEnabled !== undefined) out.http.enabled = httpEnabled;
|
|
@@ -247,6 +258,22 @@ function validateAndNormalizeConfig(cfg) {
|
|
|
247
258
|
}
|
|
248
259
|
}
|
|
249
260
|
|
|
261
|
+
// compat.versionMismatch
|
|
262
|
+
if (cfg.compat?.versionMismatch) {
|
|
263
|
+
const raw = String(cfg.compat.versionMismatch).trim().toLowerCase();
|
|
264
|
+
const normalized =
|
|
265
|
+
raw === 'warning' ? 'warn' : raw === 'none' || raw === 'ignore' ? 'off' : raw;
|
|
266
|
+
const allowed = new Set(['warn', 'error', 'off']);
|
|
267
|
+
if (!allowed.has(normalized)) {
|
|
268
|
+
console.error(
|
|
269
|
+
`[unity-mcp-server] WARN: Invalid UNITY_MCP_VERSION_MISMATCH (${cfg.compat.versionMismatch}); using default warn`
|
|
270
|
+
);
|
|
271
|
+
cfg.compat.versionMismatch = 'warn';
|
|
272
|
+
} else {
|
|
273
|
+
cfg.compat.versionMismatch = normalized;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
250
277
|
// unity hosts
|
|
251
278
|
if (typeof cfg.unity.unityHost !== 'string' || cfg.unity.unityHost.trim() === '') {
|
|
252
279
|
cfg.unity.unityHost = 'localhost';
|
package/src/core/projectInfo.js
CHANGED
|
@@ -18,17 +18,32 @@ const looksLikeUnityProjectRoot = dir => {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const inferUnityProjectRootFromDir = startDir => {
|
|
21
|
+
// First, search immediate child directories (1 level deep)
|
|
22
|
+
try {
|
|
23
|
+
const entries = fs.readdirSync(startDir, { withFileTypes: true });
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (!entry.isDirectory()) continue;
|
|
26
|
+
// Skip hidden directories and common non-project directories
|
|
27
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
28
|
+
const childDir = path.join(startDir, entry.name);
|
|
29
|
+
if (looksLikeUnityProjectRoot(childDir)) return childDir;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Fall through to upward search
|
|
33
|
+
}
|
|
34
|
+
// If not found in children, walk up to find a Unity project
|
|
21
35
|
try {
|
|
22
36
|
let dir = startDir;
|
|
23
37
|
const { root } = path.parse(dir);
|
|
24
38
|
while (true) {
|
|
25
39
|
if (looksLikeUnityProjectRoot(dir)) return dir;
|
|
26
|
-
if (dir === root)
|
|
40
|
+
if (dir === root) break;
|
|
27
41
|
dir = path.dirname(dir);
|
|
28
42
|
}
|
|
29
43
|
} catch {
|
|
30
|
-
|
|
44
|
+
// Ignore errors (e.g., permission denied)
|
|
31
45
|
}
|
|
46
|
+
return null;
|
|
32
47
|
};
|
|
33
48
|
|
|
34
49
|
const resolveDefaultCodeIndexRoot = projectRoot => {
|
|
@@ -23,6 +23,11 @@ export class UnityConnection extends EventEmitter {
|
|
|
23
23
|
this.maxInFlight = 1; // process one command at a time by default
|
|
24
24
|
this.connectedAt = null; // Timestamp when connection was established
|
|
25
25
|
this.hasConnectedOnce = false;
|
|
26
|
+
|
|
27
|
+
// Version compatibility between Node package and Unity package
|
|
28
|
+
this._versionCompatibilityChecked = false;
|
|
29
|
+
this._unityPackageVersion = null;
|
|
30
|
+
this._versionMismatchError = null;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
@@ -360,6 +365,10 @@ export class UnityConnection extends EventEmitter {
|
|
|
360
365
|
* @returns {Promise<any>} - Response from Unity
|
|
361
366
|
*/
|
|
362
367
|
async sendCommand(type, params = {}) {
|
|
368
|
+
if (this._versionMismatchError) {
|
|
369
|
+
throw this._versionMismatchError;
|
|
370
|
+
}
|
|
371
|
+
|
|
363
372
|
logger.info(`[Unity] enqueue sendCommand: ${type}`, { connected: this.connected });
|
|
364
373
|
|
|
365
374
|
if (!this.connected) {
|
|
@@ -380,6 +389,18 @@ export class UnityConnection extends EventEmitter {
|
|
|
380
389
|
_pumpQueue() {
|
|
381
390
|
if (!this.connected) return;
|
|
382
391
|
if (this.inFlight >= this.maxInFlight) return;
|
|
392
|
+
|
|
393
|
+
if (this._versionMismatchError) {
|
|
394
|
+
const err = this._versionMismatchError;
|
|
395
|
+
while (this.sendQueue.length) {
|
|
396
|
+
const task = this.sendQueue.shift();
|
|
397
|
+
try {
|
|
398
|
+
task.outerReject(err);
|
|
399
|
+
} catch {}
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
383
404
|
const task = this.sendQueue.shift();
|
|
384
405
|
if (!task) return;
|
|
385
406
|
|
|
@@ -447,6 +468,8 @@ export class UnityConnection extends EventEmitter {
|
|
|
447
468
|
`[Unity] Parsed response id=${response?.id || 'n/a'} status=${response?.status || (response?.success === false ? 'error' : 'success')}`
|
|
448
469
|
);
|
|
449
470
|
|
|
471
|
+
this._checkVersionCompatibility(response);
|
|
472
|
+
|
|
450
473
|
const id = response?.id != null ? String(response.id) : null;
|
|
451
474
|
|
|
452
475
|
const hasExplicitPending = id && this.pendingCommands.has(id);
|
|
@@ -470,10 +493,15 @@ export class UnityConnection extends EventEmitter {
|
|
|
470
493
|
logger.warning(`[Unity] Failed to parse result as JSON: ${parseError.message}`);
|
|
471
494
|
}
|
|
472
495
|
}
|
|
473
|
-
|
|
496
|
+
const responseVersion = response?.version || response?.editorState?.version;
|
|
497
|
+
if (responseVersion && result._version == null) result._version = responseVersion;
|
|
474
498
|
if (response.editorState) result._editorState = response.editorState;
|
|
475
|
-
|
|
476
|
-
|
|
499
|
+
if (this._versionMismatchError) {
|
|
500
|
+
pending.reject(this._versionMismatchError);
|
|
501
|
+
} else {
|
|
502
|
+
logger.info(`[Unity] Command ${targetId} resolved successfully`);
|
|
503
|
+
pending.resolve(result);
|
|
504
|
+
}
|
|
477
505
|
} else if (response.status === 'error' || response.success === false) {
|
|
478
506
|
logger.error(`[Unity] Command ${targetId} failed:`, response.error);
|
|
479
507
|
const err = new Error(response.error || 'Command failed');
|
|
@@ -491,6 +519,47 @@ export class UnityConnection extends EventEmitter {
|
|
|
491
519
|
this.emit('message', response);
|
|
492
520
|
}
|
|
493
521
|
|
|
522
|
+
_checkVersionCompatibility(response) {
|
|
523
|
+
if (this._versionCompatibilityChecked) return;
|
|
524
|
+
|
|
525
|
+
const policy = String(config?.compat?.versionMismatch || 'warn')
|
|
526
|
+
.trim()
|
|
527
|
+
.toLowerCase();
|
|
528
|
+
if (policy === 'off') {
|
|
529
|
+
this._versionCompatibilityChecked = true;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const unityVersionRaw = response?.version ?? response?.editorState?.version;
|
|
534
|
+
if (typeof unityVersionRaw !== 'string') return;
|
|
535
|
+
const unityVersion = unityVersionRaw.trim();
|
|
536
|
+
if (!unityVersion || unityVersion.toLowerCase() === 'unknown') return;
|
|
537
|
+
|
|
538
|
+
const nodeVersion = String(config?.server?.version || '').trim();
|
|
539
|
+
if (!nodeVersion || nodeVersion.toLowerCase() === 'unknown') {
|
|
540
|
+
this._versionCompatibilityChecked = true;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
this._unityPackageVersion = unityVersion;
|
|
545
|
+
this._versionCompatibilityChecked = true;
|
|
546
|
+
|
|
547
|
+
if (unityVersion === nodeVersion) return;
|
|
548
|
+
|
|
549
|
+
const message =
|
|
550
|
+
`Version mismatch detected: Node package v${nodeVersion} != Unity package v${unityVersion}. ` +
|
|
551
|
+
`Update @akiojin/unity-mcp-server or the Unity UPM package so they match.`;
|
|
552
|
+
|
|
553
|
+
if (policy === 'error') {
|
|
554
|
+
const err = new Error(message);
|
|
555
|
+
err.code = 'UNITY_VERSION_MISMATCH';
|
|
556
|
+
this._versionMismatchError = err;
|
|
557
|
+
logger.error(message);
|
|
558
|
+
} else {
|
|
559
|
+
logger.warning(`${message} (Set UNITY_MCP_VERSION_MISMATCH=error to fail fast.)`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
494
563
|
/**
|
|
495
564
|
* Sends a ping command to Unity
|
|
496
565
|
* @returns {Promise<any>}
|