@akiojin/unity-mcp-server 3.1.0 → 3.2.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.
@@ -34,7 +34,7 @@ const args = process.argv.slice(2);
34
34
  const command = args[0] && !args[0].startsWith('--') ? args[0] : null;
35
35
  const rest = command ? args.slice(1) : args;
36
36
 
37
- let httpEnabled = false;
37
+ let httpEnabled;
38
38
  let httpPort;
39
39
  let stdioEnabled = true;
40
40
  let telemetryEnabled;
@@ -114,11 +114,12 @@ async function main() {
114
114
 
115
115
  // Start MCP server (dynamic import)
116
116
  const { startServer } = await import('../src/core/server.js');
117
+ const http = {};
118
+ if (httpEnabled !== undefined) http.enabled = httpEnabled;
119
+ if (httpPort !== undefined) http.port = httpPort;
120
+
117
121
  await startServer({
118
- http: {
119
- enabled: httpEnabled,
120
- port: httpPort
121
- },
122
+ http: Object.keys(http).length > 0 ? http : undefined,
122
123
  telemetry: telemetryEnabled === undefined ? undefined : { enabled: telemetryEnabled },
123
124
  stdioEnabled
124
125
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "3.1.0",
3
+ "version": "3.2.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",
@@ -1,12 +1,8 @@
1
1
  import fs from 'fs';
2
- import os from 'os';
3
2
  import path from 'path';
4
3
  import * as findUpPkg from 'find-up';
5
4
  import { MCPLogger } from './mcpLogger.js';
6
5
 
7
- // Diagnostic log: confirm module loading reached this point
8
- process.stderr.write('[unity-mcp-server] Config module loading...\n');
9
-
10
6
  function findUpSyncCompat(matcher, options = {}) {
11
7
  if (typeof matcher === 'function') {
12
8
  let dir = options.cwd || process.cwd();
@@ -39,6 +35,30 @@ function merge(a, b) {
39
35
  return out;
40
36
  }
41
37
 
38
+ function parseBoolEnv(value) {
39
+ if (typeof value !== 'string') return undefined;
40
+ const v = value.trim().toLowerCase();
41
+ if (v === '') return undefined;
42
+ if (v === '1' || v === 'true' || v === 'yes' || v === 'y' || v === 'on') return true;
43
+ if (v === '0' || v === 'false' || v === 'no' || v === 'n' || v === 'off') return false;
44
+ return undefined;
45
+ }
46
+
47
+ function parseIntEnv(value) {
48
+ if (typeof value !== 'string') return undefined;
49
+ const v = value.trim();
50
+ if (v === '') return undefined;
51
+ const n = Number.parseInt(v, 10);
52
+ return Number.isFinite(n) ? n : undefined;
53
+ }
54
+
55
+ function envString(key) {
56
+ const raw = process.env[key];
57
+ if (typeof raw !== 'string') return undefined;
58
+ const v = raw.trim();
59
+ return v.length > 0 ? v : undefined;
60
+ }
61
+
42
62
  function resolvePackageVersion() {
43
63
  const candidates = [];
44
64
 
@@ -71,16 +91,23 @@ function resolvePackageVersion() {
71
91
  const baseConfig = {
72
92
  // Unity connection settings
73
93
  unity: {
74
- unityHost: null,
75
- mcpHost: null,
76
- bindHost: null,
94
+ unityHost: 'localhost',
95
+ mcpHost: 'localhost',
96
+ bindHost: 'localhost',
77
97
  port: 6400,
78
98
  reconnectDelay: 1000,
79
99
  maxReconnectDelay: 30000,
80
100
  reconnectBackoffMultiplier: 2,
101
+ connectTimeout: 3000,
81
102
  commandTimeout: 30000
82
103
  },
83
104
 
105
+ // Project settings (primarily for code index paths)
106
+ project: {
107
+ root: null,
108
+ codeIndexRoot: null
109
+ },
110
+
84
111
  // Server settings
85
112
  server: {
86
113
  name: 'unity-mcp-server',
@@ -136,83 +163,124 @@ const baseConfig = {
136
163
  }
137
164
  };
138
165
 
139
- /**
140
- * External config resolution:
141
- * - Uses the nearest `.unity/config.json` found by walking up from the current working directory.
142
- * - Intentionally ignores `~/.unity/config.json` (user-global).
143
- */
144
- function loadExternalConfig() {
145
- if (typeof findUpSyncCompat !== 'function') {
146
- return {};
166
+ function loadEnvConfig() {
167
+ const unityHost = envString('UNITY_MCP_UNITY_HOST');
168
+ const mcpHost = envString('UNITY_MCP_MCP_HOST');
169
+ const unityPort = parseIntEnv(process.env.UNITY_MCP_PORT);
170
+
171
+ const projectRoot = envString('UNITY_PROJECT_ROOT');
172
+
173
+ const logLevel = envString('UNITY_MCP_LOG_LEVEL');
174
+
175
+ const httpEnabled = parseBoolEnv(process.env.UNITY_MCP_HTTP_ENABLED);
176
+ const httpPort = parseIntEnv(process.env.UNITY_MCP_HTTP_PORT);
177
+
178
+ const telemetryEnabled = parseBoolEnv(process.env.UNITY_MCP_TELEMETRY_ENABLED);
179
+ const lspRequestTimeoutMs = parseIntEnv(process.env.UNITY_MCP_LSP_REQUEST_TIMEOUT_MS);
180
+
181
+ const out = {};
182
+
183
+ if (unityHost || mcpHost || unityPort !== undefined) {
184
+ out.unity = {};
185
+ if (unityHost) out.unity.unityHost = unityHost;
186
+ if (mcpHost) out.unity.mcpHost = mcpHost;
187
+ if (unityPort !== undefined) out.unity.port = unityPort;
188
+ if (out.unity.unityHost) out.unity.bindHost = out.unity.unityHost;
147
189
  }
148
- let userGlobalPath = null;
149
- try {
150
- const homeDir = os.homedir();
151
- userGlobalPath = homeDir ? path.resolve(homeDir, '.unity', 'config.json') : null;
152
- } catch {}
153
190
 
154
- const projectPath = findUpSyncCompat(
155
- directory => {
156
- const candidate = path.resolve(directory, '.unity', 'config.json');
157
- if (userGlobalPath && path.resolve(candidate) === userGlobalPath) return undefined;
158
- return fs.existsSync(candidate) ? candidate : undefined;
159
- },
160
- { cwd: process.cwd() }
161
- );
191
+ if (projectRoot) {
192
+ out.project = {};
193
+ if (projectRoot) out.project.root = projectRoot;
194
+ }
162
195
 
163
- if (!projectPath) return {};
164
- try {
165
- const raw = fs.readFileSync(projectPath, 'utf8');
166
- const json = JSON.parse(raw);
167
- const out = json && typeof json === 'object' ? json : {};
168
- out.__configPath = projectPath;
169
- return out;
170
- } catch (e) {
171
- return { __configLoadError: `${projectPath}: ${e.message}` };
196
+ if (logLevel) {
197
+ out.logging = { level: logLevel };
172
198
  }
173
- }
174
199
 
175
- const external = loadExternalConfig();
176
- export const config = merge(baseConfig, external);
200
+ if (httpEnabled !== undefined || httpPort !== undefined) {
201
+ out.http = {};
202
+ if (httpEnabled !== undefined) out.http.enabled = httpEnabled;
203
+ if (httpPort !== undefined) out.http.port = httpPort;
204
+ }
177
205
 
178
- const normalizeUnityConfig = () => {
179
- const unityConfig = config.unity || (config.unity = {});
206
+ if (telemetryEnabled !== undefined) {
207
+ out.telemetry = { enabled: telemetryEnabled };
208
+ }
180
209
 
181
- // Legacy aliases coming from config files
182
- const legacyHost = unityConfig.host;
183
- const legacyClientHost = unityConfig.clientHost;
184
- const legacyBindHost = unityConfig.bindHost;
210
+ if (lspRequestTimeoutMs !== undefined) {
211
+ out.lsp = { requestTimeoutMs: lspRequestTimeoutMs };
212
+ }
185
213
 
186
- if (!unityConfig.unityHost) {
187
- unityConfig.unityHost = legacyBindHost || legacyHost || 'localhost';
214
+ return out;
215
+ }
216
+
217
+ function validateAndNormalizeConfig(cfg) {
218
+ // Unity port
219
+ if (!Number.isInteger(cfg.unity.port) || cfg.unity.port <= 0 || cfg.unity.port >= 65536) {
220
+ console.error(
221
+ `[unity-mcp-server] WARN: Invalid UNITY_MCP_PORT (${cfg.unity.port}); using default 6400`
222
+ );
223
+ cfg.unity.port = 6400;
188
224
  }
189
225
 
190
- if (!unityConfig.mcpHost) {
191
- unityConfig.mcpHost = legacyClientHost || legacyHost || unityConfig.unityHost;
226
+ // HTTP port
227
+ if (cfg.http?.port !== undefined) {
228
+ if (!Number.isInteger(cfg.http.port) || cfg.http.port <= 0 || cfg.http.port >= 65536) {
229
+ console.error(
230
+ `[unity-mcp-server] WARN: Invalid UNITY_MCP_HTTP_PORT (${cfg.http.port}); using default 6401`
231
+ );
232
+ cfg.http.port = 6401;
233
+ }
192
234
  }
193
235
 
194
- // Keep bindHost for backwards compatibility with legacy code paths
195
- if (!unityConfig.bindHost) {
196
- unityConfig.bindHost = legacyBindHost || unityConfig.unityHost;
236
+ // logging.level
237
+ if (cfg.logging?.level) {
238
+ const level = String(cfg.logging.level).toLowerCase();
239
+ const allowed = new Set(['debug', 'info', 'warn', 'warning', 'error']);
240
+ if (!allowed.has(level)) {
241
+ console.error(
242
+ `[unity-mcp-server] WARN: Invalid UNITY_MCP_LOG_LEVEL (${cfg.logging.level}); using default info`
243
+ );
244
+ cfg.logging.level = 'info';
245
+ } else {
246
+ cfg.logging.level = level === 'warning' ? 'warn' : level;
247
+ }
197
248
  }
198
249
 
199
- // Maintain legacy properties so older handlers keep working
200
- unityConfig.host = unityConfig.unityHost;
201
- unityConfig.clientHost = unityConfig.mcpHost;
202
- };
250
+ // unity hosts
251
+ if (typeof cfg.unity.unityHost !== 'string' || cfg.unity.unityHost.trim() === '') {
252
+ cfg.unity.unityHost = 'localhost';
253
+ }
254
+ if (typeof cfg.unity.mcpHost !== 'string' || cfg.unity.mcpHost.trim() === '') {
255
+ cfg.unity.mcpHost = cfg.unity.unityHost;
256
+ }
257
+ cfg.unity.bindHost = cfg.unity.unityHost;
203
258
 
204
- normalizeUnityConfig();
259
+ // project roots (keep as-is; resolved later)
260
+ if (cfg.project?.root && typeof cfg.project.root !== 'string') {
261
+ cfg.project.root = null;
262
+ }
263
+ if (cfg.project?.codeIndexRoot && typeof cfg.project.codeIndexRoot !== 'string') {
264
+ cfg.project.codeIndexRoot = null;
265
+ }
205
266
 
206
- // Workspace root detection: directory that contains .unity/config.json used
207
- const initialCwd = process.cwd();
208
- let workspaceRoot = initialCwd;
209
- try {
210
- if (config.__configPath) {
211
- const cfgDir = path.dirname(config.__configPath); // <workspace>/.unity
212
- workspaceRoot = path.dirname(cfgDir); // <workspace>
267
+ // lsp timeout sanity
268
+ if (cfg.lsp?.requestTimeoutMs !== undefined) {
269
+ const t = Number(cfg.lsp.requestTimeoutMs);
270
+ if (!Number.isFinite(t) || t <= 0) {
271
+ console.error(
272
+ `[unity-mcp-server] WARN: Invalid UNITY_MCP_LSP_REQUEST_TIMEOUT_MS (${cfg.lsp.requestTimeoutMs}); using default 60000`
273
+ );
274
+ cfg.lsp.requestTimeoutMs = 60000;
275
+ }
213
276
  }
214
- } catch {}
215
- export const WORKSPACE_ROOT = workspaceRoot;
277
+ }
278
+
279
+ export const config = merge(baseConfig, loadEnvConfig());
280
+ validateAndNormalizeConfig(config);
281
+
282
+ // Workspace root: current working directory (used for cache/capture roots)
283
+ export const WORKSPACE_ROOT = process.cwd();
216
284
 
217
285
  /**
218
286
  * Logger utility
@@ -224,18 +292,9 @@ export const WORKSPACE_ROOT = workspaceRoot;
224
292
  */
225
293
  export const logger = new MCPLogger(config);
226
294
 
227
- // Late log if external config failed to load
228
- if (config.__configLoadError) {
229
- console.error(
230
- `${baseConfig.logging.prefix} WARN: Failed to load external config: ${config.__configLoadError}`
231
- );
232
- delete config.__configLoadError;
233
- }
234
-
235
295
  // Startup debug log: output config info to stderr for troubleshooting
236
296
  // This helps diagnose connection issues (especially in WSL2/Docker environments)
237
297
  console.error(`[unity-mcp-server] Startup config:`);
238
- console.error(`[unity-mcp-server] Config file: ${config.__configPath || '(not found)'}`);
239
298
  console.error(
240
299
  `[unity-mcp-server] Unity host: ${config.unity.mcpHost || config.unity.unityHost || 'localhost'}`
241
300
  );
@@ -46,7 +46,7 @@ export class ProjectInfoProvider {
46
46
 
47
47
  async get() {
48
48
  if (this.cached) return this.cached;
49
- // Env-driven project root override (primarily for tests)
49
+ // Env-driven project root override (explicit, deterministic)
50
50
  const envRootRaw = process.env.UNITY_PROJECT_ROOT;
51
51
  if (typeof envRootRaw === 'string' && envRootRaw.trim().length > 0) {
52
52
  const envRoot = envRootRaw.trim();
@@ -62,7 +62,7 @@ export class ProjectInfoProvider {
62
62
  return this.cached;
63
63
  }
64
64
 
65
- // Config-driven project root (no env fallback)
65
+ // Config-driven project root (env is mapped into config.project.root)
66
66
  const cfgRootRaw = config?.project?.root;
67
67
  if (typeof cfgRootRaw === 'string' && cfgRootRaw.trim().length > 0) {
68
68
  const cfgRoot = cfgRootRaw.trim();
@@ -122,13 +122,8 @@ export class ProjectInfoProvider {
122
122
  return this.cached;
123
123
  }
124
124
 
125
- if (typeof cfgRootRaw === 'string') {
126
- throw new Error(
127
- 'project.root is configured but empty. Set a valid path in .unity/config.json.'
128
- );
129
- }
130
125
  throw new Error(
131
- 'Unable to resolve Unity project root. Start the server inside a Unity project (directory containing Assets/ and Packages/) or configure project.root in .unity/config.json.'
126
+ 'Unable to resolve Unity project root. Start the server inside a Unity project (directory containing Assets/ and Packages/) or set UNITY_PROJECT_ROOT.'
132
127
  );
133
128
  }
134
129
 
@@ -91,6 +91,12 @@ async function ensureInitialized() {
91
91
  // Initialize server
92
92
  export async function startServer(options = {}) {
93
93
  try {
94
+ // MCP stdio transport requires stdout to stay clean (JSON-RPC only).
95
+ // Guard against accidental console.log usage breaking the protocol.
96
+ if (options.stdioEnabled !== false && process.env.UNITY_MCP_ALLOW_STDOUT !== '1') {
97
+ console.log = (...args) => console.error(...args);
98
+ }
99
+
94
100
  // Step 1: Load minimal config for server metadata
95
101
  // (config import is lightweight; avoid importing the MCP TS SDK on startup)
96
102
  const { config: serverConfig, logger: serverLogger } = await import('./config.js');
@@ -0,0 +1,10 @@
1
+ const TOOL_NAME_TO_UNITY_COMMAND_TYPE = Object.freeze({
2
+ // Animator tools: tool name (MCP) -> Unity command type (TCP)
3
+ analysis_animator_state_get: 'get_animator_state',
4
+ analysis_animator_runtime_info_get: 'get_animator_runtime_info'
5
+ });
6
+
7
+ export function normalizeUnityCommandType(type) {
8
+ if (typeof type !== 'string' || type.length === 0) return type;
9
+ return TOOL_NAME_TO_UNITY_COMMAND_TYPE[type] ?? type;
10
+ }
@@ -1,6 +1,7 @@
1
1
  import net from 'net';
2
2
  import { EventEmitter } from 'events';
3
3
  import { config, logger } from './config.js';
4
+ import { normalizeUnityCommandType } from './unityCommandType.js';
4
5
 
5
6
  /**
6
7
  * Manages TCP connection to Unity Editor
@@ -22,6 +23,7 @@ export class UnityConnection extends EventEmitter {
22
23
  this.inFlight = 0;
23
24
  this.maxInFlight = 1; // process one command at a time by default
24
25
  this.connectedAt = null; // Timestamp when connection was established
26
+ this.hasConnectedOnce = false;
25
27
  }
26
28
 
27
29
  /**
@@ -86,6 +88,7 @@ export class UnityConnection extends EventEmitter {
86
88
  this.connected = true;
87
89
  this.reconnectAttempts = 0;
88
90
  this.connectPromise = null;
91
+ this.hasConnectedOnce = true;
89
92
  this.emit('connected');
90
93
  this._pumpQueue(); // flush any queued commands that arrived during reconnect
91
94
  settle(resolve);
@@ -120,7 +123,7 @@ export class UnityConnection extends EventEmitter {
120
123
  // Destroy the socket to clean up properly
121
124
  this.socket.destroy();
122
125
  this.isDisconnecting = false;
123
- reject(error);
126
+ reject(wrapUnityConnectError(error, targetHost, config.unity.port));
124
127
  } else if (this.connected) {
125
128
  // Force close to trigger reconnect logic
126
129
  try {
@@ -175,9 +178,11 @@ export class UnityConnection extends EventEmitter {
175
178
  this.socket.removeAllListeners();
176
179
  this.socket.destroy();
177
180
  this.connectPromise = null;
178
- settle(reject, new Error('Connection timeout'));
181
+ const timeoutError = new Error('Connection timeout');
182
+ timeoutError.code = 'ETIMEDOUT';
183
+ settle(reject, wrapUnityConnectError(timeoutError, targetHost, config.unity.port));
179
184
  }
180
- }, config.unity.commandTimeout);
185
+ }, config.unity.connectTimeout ?? config.unity.commandTimeout);
181
186
  });
182
187
  return this.connectPromise;
183
188
  }
@@ -356,18 +361,19 @@ export class UnityConnection extends EventEmitter {
356
361
  * @returns {Promise<any>} - Response from Unity
357
362
  */
358
363
  async sendCommand(type, params = {}) {
359
- logger.info(`[Unity] enqueue sendCommand: ${type}`, { connected: this.connected });
364
+ const normalizedType = normalizeUnityCommandType(type);
365
+ logger.info(`[Unity] enqueue sendCommand: ${normalizedType}`, { connected: this.connected });
360
366
 
361
367
  if (!this.connected) {
362
368
  logger.warning('[Unity] Not connected; waiting for reconnection before sending command');
363
369
  await this.ensureConnected({
364
- timeoutMs: config.unity.commandTimeout
370
+ timeoutMs: config.unity.connectTimeout ?? config.unity.commandTimeout
365
371
  });
366
372
  }
367
373
 
368
374
  // Create an external promise that will resolve when Unity responds
369
375
  return new Promise((outerResolve, outerReject) => {
370
- const task = { type, params, outerResolve, outerReject };
376
+ const task = { type: normalizedType, params, outerResolve, outerReject };
371
377
  this.sendQueue.push(task);
372
378
  this._pumpQueue();
373
379
  });
@@ -518,6 +524,13 @@ export class UnityConnection extends EventEmitter {
518
524
  if (/test environment/i.test(msg)) {
519
525
  throw error;
520
526
  }
527
+ if (
528
+ !this.hasConnectedOnce &&
529
+ (error?.code === 'UNITY_CONNECTION_REFUSED' || error?.code === 'ECONNREFUSED')
530
+ ) {
531
+ // Fail fast for the initial connection when Unity isn't listening.
532
+ throw error;
533
+ }
521
534
  }
522
535
 
523
536
  if (this.connected) return;
@@ -525,8 +538,9 @@ export class UnityConnection extends EventEmitter {
525
538
  }
526
539
 
527
540
  if (!this.connected) {
541
+ const targetHost = config.unity.mcpHost || config.unity.unityHost || 'localhost';
528
542
  const error = new Error(
529
- `Failed to reconnect to Unity within ${timeoutMs}ms${lastError ? `: ${lastError.message}` : ''}`
543
+ `Failed to connect to Unity at ${targetHost}:${config.unity.port} within ${timeoutMs}ms${lastError ? `: ${lastError.message}` : ''}`
530
544
  );
531
545
  error.code = 'UNITY_RECONNECT_TIMEOUT';
532
546
  throw error;
@@ -545,3 +559,33 @@ export class UnityConnection extends EventEmitter {
545
559
  function sleep(ms) {
546
560
  return new Promise(resolve => setTimeout(resolve, Math.max(0, ms || 0)));
547
561
  }
562
+
563
+ function wrapUnityConnectError(error, host, port) {
564
+ const code = error?.code || '';
565
+ if (code === 'ECONNREFUSED') {
566
+ const hint = buildUnityConnectionHint(host, port);
567
+ const wrapped = new Error(
568
+ `Unity TCP connection refused (ECONNREFUSED) at ${host}:${port}. ${hint}`
569
+ );
570
+ wrapped.code = 'UNITY_CONNECTION_REFUSED';
571
+ wrapped.cause = error;
572
+ return wrapped;
573
+ }
574
+ if (code === 'ETIMEDOUT') {
575
+ const hint = buildUnityConnectionHint(host, port);
576
+ const wrapped = new Error(`Unity TCP Connection timeout at ${host}:${port}. ${hint}`);
577
+ wrapped.code = 'UNITY_CONNECTION_TIMEOUT';
578
+ wrapped.cause = error;
579
+ return wrapped;
580
+ }
581
+ return error;
582
+ }
583
+
584
+ function buildUnityConnectionHint(_host, _port) {
585
+ const configPath = config.__configPath || '.unity/config.json';
586
+ return (
587
+ `Start Unity Editor and ensure the Unity MCP package is running (TCP listener). ` +
588
+ `Check ${configPath} (unity.mcpHost/unity.port). ` +
589
+ `If using WSL2/Docker → Windows Unity, set unity.mcpHost=host.docker.internal.`
590
+ );
591
+ }
@@ -213,42 +213,48 @@ export class ScriptRefsFindToolHandler extends BaseToolHandler {
213
213
  }
214
214
  }
215
215
 
216
- // Phase 3.2: Build pathTable for deduplication (62% size reduction)
217
- const pathSet = new Set();
218
- for (const [p] of perFile) {
219
- pathSet.add(p);
220
- }
221
- const pathTable = [...pathSet];
222
- const pathIndex = new Map(pathTable.map((p, i) => [p, i]));
223
-
224
- // Pagination and byte limits with compact format
216
+ // Phase 3.3: Grouped output format (more compact, LLM-friendly)
217
+ // Pagination and byte limits with grouped format
225
218
  const results = [];
226
219
  let bytes = 0;
220
+ let total = 0;
221
+ let truncated = false;
222
+
227
223
  for (const [filePath, arr] of perFile) {
228
- const fileId = pathIndex.get(filePath);
224
+ const references = [];
229
225
  for (const it of arr) {
230
- // Compact format: fileId reference + only essential fields
231
- const compact = {
232
- fileId,
226
+ const ref = {
233
227
  line: it.line,
234
228
  column: it.column,
235
229
  snippet: it.snippet
236
230
  };
237
- const json = JSON.stringify(compact);
238
- const size = Buffer.byteLength(json, 'utf8');
239
- if (results.length >= pageSize || bytes + size > maxBytes) {
240
- return { success: true, pathTable, results, total: results.length, truncated: true };
231
+ const refJson = JSON.stringify(ref);
232
+ const refSize = Buffer.byteLength(refJson, 'utf8');
233
+
234
+ // Check limits before adding
235
+ if (total >= pageSize || bytes + refSize > maxBytes) {
236
+ truncated = true;
237
+ break;
241
238
  }
242
- results.push(compact);
243
- bytes += size;
239
+ references.push(ref);
240
+ bytes += refSize;
241
+ total++;
244
242
  }
243
+
244
+ if (references.length > 0) {
245
+ results.push({ path: filePath, references });
246
+ }
247
+
248
+ if (truncated) break;
245
249
  }
246
250
 
247
251
  // Edge case: extreme limits
248
252
  const extremeLimits = pageSize <= 1 || maxBytes <= 1;
249
- const truncated = extremeLimits && results.length === 0 ? true : false;
253
+ if (extremeLimits && results.length === 0) {
254
+ truncated = true;
255
+ }
250
256
 
251
- return { success: true, pathTable, results, total: results.length, truncated };
257
+ return { success: true, results, total, truncated };
252
258
  }
253
259
  }
254
260
 
@@ -161,9 +161,8 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
161
161
  }
162
162
  const matcher = buildMatcher(patternType, pattern, flags);
163
163
 
164
+ // Phase 3.3: Grouped output format (more compact, LLM-friendly)
164
165
  const results = [];
165
- const pathTable = [];
166
- const pathId = new Map();
167
166
  let bytes = 0;
168
167
  let afterFound = !startAfter;
169
168
 
@@ -200,13 +199,8 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
200
199
  }
201
200
  if (matches === 0) continue;
202
201
 
203
- const id = pathId.has(rel)
204
- ? pathId.get(rel)
205
- : (pathTable.push(rel) - 1, pathTable.length - 1);
206
- pathId.set(rel, id);
207
-
208
202
  const lineRanges = toRanges(matchedLines);
209
- const item = { fileId: id, lineRanges };
203
+ const item = { path: rel, lineRanges };
210
204
 
211
205
  if (normalizedReturnMode === 'snippets') {
212
206
  // Build minimal snippets around first few matches
@@ -229,10 +223,9 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
229
223
  return {
230
224
  success: true,
231
225
  total: results.length,
232
- pathTable,
233
226
  results,
234
227
  cursor:
235
- results.length && results.length >= pageSize ? pathTable[pathTable.length - 1] : null
228
+ results.length && results.length >= pageSize ? results[results.length - 1].path : null
236
229
  };
237
230
  } catch (e) {
238
231
  logger.error(`[script_search] failed: ${e.message}`);
@@ -22,7 +22,7 @@ export class ScriptSymbolFindToolHandler extends BaseToolHandler {
22
22
  type: 'string',
23
23
  enum: ['assets', 'packages', 'embedded', 'all'],
24
24
  description:
25
- 'Search scope: assets (Assets/), packages (Packages/), embedded, or all (default: all).'
25
+ 'Search scope: assets (Assets/), packages (Packages/), embedded, or all (default: assets).'
26
26
  },
27
27
  exact: {
28
28
  type: 'boolean',
@@ -110,18 +110,15 @@ export class ScriptSymbolFindToolHandler extends BaseToolHandler {
110
110
  filteredRows = filteredRows.filter(r => r.name === target);
111
111
  }
112
112
 
113
- // Phase 3.1: Optimized output format with pathTable (60% size reduction)
114
- // Build pathTable for deduplication
115
- const pathSet = new Set();
113
+ // Phase 3.2: Grouped output format (40% more compact than pathTable)
114
+ // Group symbols by file path
115
+ const grouped = new Map();
116
+ let total = 0;
116
117
  for (const r of filteredRows) {
117
- pathSet.add((r.path || '').replace(/\\/g, '/'));
118
- }
119
- const pathTable = [...pathSet];
120
- const pathIndex = new Map(pathTable.map((p, i) => [p, i]));
121
-
122
- // Build compact results with fileId reference
123
- const results = filteredRows.map(r => {
124
118
  const p = (r.path || '').replace(/\\/g, '/');
119
+ if (!grouped.has(p)) {
120
+ grouped.set(p, []);
121
+ }
125
122
  const symbol = {
126
123
  name: r.name,
127
124
  kind: r.kind,
@@ -131,10 +128,16 @@ export class ScriptSymbolFindToolHandler extends BaseToolHandler {
131
128
  // Only include non-null optional fields
132
129
  if (r.ns) symbol.namespace = r.ns;
133
130
  if (r.container) symbol.container = r.container;
131
+ grouped.get(p).push(symbol);
132
+ total++;
133
+ }
134
134
 
135
- return { fileId: pathIndex.get(p), symbol };
136
- });
135
+ // Convert to array format
136
+ const results = [];
137
+ for (const [path, symbols] of grouped) {
138
+ results.push({ path, symbols });
139
+ }
137
140
 
138
- return { success: true, pathTable, results, total: results.length };
141
+ return { success: true, results, total };
139
142
  }
140
143
  }
@@ -47,7 +47,7 @@ export async function analyzeSceneContentsHandler(unityConnection, args) {
47
47
  }
48
48
 
49
49
  // Send command to Unity with provided parameters
50
- const result = await unityConnection.sendCommand('analysis_scene_contents_analyze', args);
50
+ const result = await unityConnection.sendCommand('analyze_scene_contents', args);
51
51
 
52
52
  // The unityConnection.sendCommand already extracts the result field
53
53
  // from the response, so we access properties directly on result
@@ -48,10 +48,23 @@ export async function findByComponentHandler(unityConnection, args) {
48
48
  }
49
49
 
50
50
  // Send command to Unity
51
- const result = await unityConnection.sendCommand('analysis_component_find', args);
51
+ const result = await unityConnection.sendCommand('find_by_component', args);
52
52
 
53
- // Handle Unity response
54
- if (result.status === 'error') {
53
+ // The unityConnection.sendCommand already extracts the result field
54
+ // from the response, so we access properties directly on result
55
+ if (!result || typeof result === 'string') {
56
+ return {
57
+ content: [
58
+ {
59
+ type: 'text',
60
+ text: `Failed to find GameObjects: Invalid response format`
61
+ }
62
+ ],
63
+ isError: true
64
+ };
65
+ }
66
+
67
+ if (result.error) {
55
68
  return {
56
69
  content: [
57
70
  {
@@ -63,12 +76,14 @@ export async function findByComponentHandler(unityConnection, args) {
63
76
  };
64
77
  }
65
78
 
79
+ const summary = result.summary || `Found ${result.totalFound ?? 0} GameObjects`;
80
+
66
81
  // Success response
67
82
  return {
68
83
  content: [
69
84
  {
70
85
  type: 'text',
71
- text: result.result.summary || `Found ${result.result.totalFound} GameObjects`
86
+ text: summary
72
87
  }
73
88
  ],
74
89
  isError: false
@@ -99,8 +99,8 @@ export async function getAnimatorStateHandler(unityConnection, args) {
99
99
  };
100
100
  }
101
101
 
102
- // Send command to Unity
103
- const result = await unityConnection.sendCommand('analysis_animator_state_get', args);
102
+ // Send command to Unity (tool name is normalized to Unity command type in UnityConnection)
103
+ const result = await unityConnection.sendCommand(getAnimatorStateToolDefinition.name, args);
104
104
 
105
105
  // Check for errors
106
106
  if (!result || typeof result === 'string') {
@@ -221,8 +221,11 @@ export async function getAnimatorRuntimeInfoHandler(unityConnection, args) {
221
221
  };
222
222
  }
223
223
 
224
- // Send command to Unity
225
- const result = await unityConnection.sendCommand('analysis_animator_runtime_info_get', args);
224
+ // Send command to Unity (tool name is normalized to Unity command type in UnityConnection)
225
+ const result = await unityConnection.sendCommand(
226
+ getAnimatorRuntimeInfoToolDefinition.name,
227
+ args
228
+ );
226
229
 
227
230
  // Check for errors
228
231
  if (!result || typeof result === 'string') {
@@ -77,7 +77,7 @@ export async function getComponentValuesHandler(unityConnection, args) {
77
77
  }
78
78
 
79
79
  // Send command to Unity
80
- const result = await unityConnection.sendCommand('analysis_component_values_get', args);
80
+ const result = await unityConnection.sendCommand('get_component_values', args);
81
81
 
82
82
  // The unityConnection.sendCommand already extracts the result field
83
83
  // from the response, so we access properties directly on result
@@ -107,13 +107,10 @@ export async function getComponentValuesHandler(unityConnection, args) {
107
107
  }
108
108
 
109
109
  // Success response - result is already the unwrapped data
110
- console.log('[DEBUG] GetComponentValues - Full result:', JSON.stringify(result, null, 2));
111
-
112
110
  let responseText = result.summary || `Component values retrieved`;
113
111
 
114
112
  // Add detailed property information if available
115
113
  if (result.properties && Object.keys(result.properties).length > 0) {
116
- console.log('[DEBUG] Properties found:', Object.keys(result.properties).length);
117
114
  responseText += '\n\nProperties:';
118
115
  for (const [key, value] of Object.entries(result.properties)) {
119
116
  if (value && typeof value === 'object') {
@@ -144,9 +141,6 @@ export async function getComponentValuesHandler(unityConnection, args) {
144
141
  }
145
142
  }
146
143
  }
147
- } else {
148
- console.log('[DEBUG] No properties found in result');
149
- console.log('[DEBUG] Result keys:', Object.keys(result));
150
144
  }
151
145
 
152
146
  // Add debug information if available
@@ -106,7 +106,7 @@ export async function getGameObjectDetailsHandler(unityConnection, args) {
106
106
  if (args.maxDepth !== undefined) params.maxDepth = args.maxDepth;
107
107
 
108
108
  // Send command to Unity
109
- const result = await unityConnection.sendCommand('analysis_gameobject_details_get', args);
109
+ const result = await unityConnection.sendCommand('get_gameobject_details', params);
110
110
 
111
111
  // The unityConnection.sendCommand already extracts the result field
112
112
  // from the response, so we access properties directly on result
@@ -75,7 +75,7 @@ export async function getInputActionsStateHandler(unityConnection, args) {
75
75
  }
76
76
 
77
77
  // Send command to Unity
78
- const result = await unityConnection.sendCommand('input_actions_state_get', args);
78
+ const result = await unityConnection.sendCommand('get_input_actions_state', args);
79
79
 
80
80
  // Check for errors
81
81
  if (!result || typeof result === 'string') {
@@ -216,7 +216,7 @@ export async function analyzeInputActionsAssetHandler(unityConnection, args) {
216
216
  }
217
217
 
218
218
  // Send command to Unity
219
- const result = await unityConnection.sendCommand('input_actions_asset_analyze', args);
219
+ const result = await unityConnection.sendCommand('analyze_input_actions_asset', args);
220
220
 
221
221
  // Check for errors
222
222
  if (!result || typeof result === 'string') {
@@ -47,10 +47,23 @@ export async function getObjectReferencesHandler(unityConnection, args) {
47
47
  }
48
48
 
49
49
  // Send command to Unity
50
- const result = await unityConnection.sendCommand('analysis_object_references_get', args);
50
+ const result = await unityConnection.sendCommand('get_object_references', args);
51
51
 
52
- // Handle Unity response
53
- if (result.status === 'error') {
52
+ // The unityConnection.sendCommand already extracts the result field
53
+ // from the response, so we access properties directly on result
54
+ if (!result || typeof result === 'string') {
55
+ return {
56
+ content: [
57
+ {
58
+ type: 'text',
59
+ text: `Failed to get object references: Invalid response format`
60
+ }
61
+ ],
62
+ isError: true
63
+ };
64
+ }
65
+
66
+ if (result.error) {
54
67
  return {
55
68
  content: [
56
69
  {
@@ -67,7 +80,7 @@ export async function getObjectReferencesHandler(unityConnection, args) {
67
80
  content: [
68
81
  {
69
82
  type: 'text',
70
- text: result.result.summary || `References analyzed for ${args.gameObjectName}`
83
+ text: result.summary || `References analyzed for ${args.gameObjectName}`
71
84
  }
72
85
  ],
73
86
  isError: false
@@ -303,7 +303,10 @@ function formatUnityResponse(result, successMessage) {
303
303
  }
304
304
 
305
305
  if (result.success) {
306
- let text = result.message || successMessage;
306
+ let text = successMessage || result.message || 'Operation completed';
307
+ if (successMessage && result.message && result.message !== successMessage) {
308
+ text += `\n${result.message}`;
309
+ }
307
310
  // Add any additional info from result
308
311
  Object.keys(result).forEach(key => {
309
312
  if (key !== 'success' && key !== 'message' && key !== 'error') {
@@ -348,7 +351,7 @@ export async function createActionMapHandler(unityConnection, args) {
348
351
  };
349
352
  }
350
353
 
351
- const result = await unityConnection.sendCommand('input_action_map_create', args);
354
+ const result = await unityConnection.sendCommand('create_action_map', args);
352
355
  return formatUnityResponse(result, `Created Action Map: ${args.mapName}`);
353
356
  } catch (error) {
354
357
  return {
@@ -377,7 +380,7 @@ export async function removeActionMapHandler(unityConnection, args) {
377
380
  };
378
381
  }
379
382
 
380
- const result = await unityConnection.sendCommand('input_action_map_remove', args);
383
+ const result = await unityConnection.sendCommand('remove_action_map', args);
381
384
  return formatUnityResponse(result, `Removed Action Map: ${args.mapName}`);
382
385
  } catch (error) {
383
386
  return {
@@ -407,7 +410,7 @@ export async function addInputActionHandler(unityConnection, args) {
407
410
  };
408
411
  }
409
412
 
410
- const result = await unityConnection.sendCommand('input_action_add', args);
413
+ const result = await unityConnection.sendCommand('add_input_action', args);
411
414
  return formatUnityResponse(result, `Added Action: ${args.actionName}`);
412
415
  } catch (error) {
413
416
  return {
@@ -436,7 +439,7 @@ export async function removeInputActionHandler(unityConnection, args) {
436
439
  };
437
440
  }
438
441
 
439
- const result = await unityConnection.sendCommand('input_action_remove', args);
442
+ const result = await unityConnection.sendCommand('remove_input_action', args);
440
443
  return formatUnityResponse(result, `Removed Action: ${args.actionName}`);
441
444
  } catch (error) {
442
445
  return {
@@ -466,7 +469,7 @@ export async function addInputBindingHandler(unityConnection, args) {
466
469
  };
467
470
  }
468
471
 
469
- const result = await unityConnection.sendCommand('input_binding_add', args);
472
+ const result = await unityConnection.sendCommand('add_input_binding', args);
470
473
  return formatUnityResponse(result, `Added Binding: ${args.path}`);
471
474
  } catch (error) {
472
475
  return {
@@ -495,7 +498,7 @@ export async function removeInputBindingHandler(unityConnection, args) {
495
498
  };
496
499
  }
497
500
 
498
- const result = await unityConnection.sendCommand('input_binding_remove', args);
501
+ const result = await unityConnection.sendCommand('remove_input_binding', args);
499
502
  return formatUnityResponse(result, 'Removed Binding');
500
503
  } catch (error) {
501
504
  return {
@@ -524,7 +527,7 @@ export async function removeAllBindingsHandler(unityConnection, args) {
524
527
  };
525
528
  }
526
529
 
527
- const result = await unityConnection.sendCommand('input_binding_remove_all', args);
530
+ const result = await unityConnection.sendCommand('remove_all_bindings', args);
528
531
  return formatUnityResponse(result, `Removed all bindings from ${args.actionName}`);
529
532
  } catch (error) {
530
533
  return {
@@ -553,7 +556,7 @@ export async function createCompositeBindingHandler(unityConnection, args) {
553
556
  };
554
557
  }
555
558
 
556
- const result = await unityConnection.sendCommand('input_binding_composite_create', args);
559
+ const result = await unityConnection.sendCommand('create_composite_binding', args);
557
560
  return formatUnityResponse(
558
561
  result,
559
562
  `Created composite binding: ${args.name || args.compositeType}`
@@ -586,7 +589,7 @@ export async function manageControlSchemesHandler(unityConnection, args) {
586
589
  };
587
590
  }
588
591
 
589
- const result = await unityConnection.sendCommand('input_control_schemes_manage', args);
592
+ const result = await unityConnection.sendCommand('manage_control_schemes', args);
590
593
  const operationText =
591
594
  args.operation === 'add' ? 'Added' : args.operation === 'remove' ? 'Removed' : 'Modified';
592
595
  return formatUnityResponse(result, `${operationText} control scheme: ${args.schemeName}`);