@akiojin/unity-mcp-server 4.0.0 → 4.1.3

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": "4.0.0",
3
+ "version": "4.1.3",
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",
@@ -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';
@@ -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) return null;
40
+ if (dir === root) break;
27
41
  dir = path.dirname(dir);
28
42
  }
29
43
  } catch {
30
- return null;
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
- if (response.version) result._version = response.version;
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
- logger.info(`[Unity] Command ${targetId} resolved successfully`);
476
- pending.resolve(result);
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>}