@akiojin/unity-mcp-server 3.0.0 → 3.2.0
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/bin/unity-mcp-server.js +6 -5
- package/package.json +1 -1
- package/src/core/config.js +135 -76
- package/src/core/projectInfo.js +23 -7
- package/src/core/server.js +6 -0
- package/src/core/unityConnection.js +47 -5
- package/src/core/workers/indexBuildWorker.js +169 -26
- package/src/handlers/scene/SceneCreateToolHandler.js +5 -6
- package/src/handlers/scene/SceneLoadToolHandler.js +5 -6
- package/src/handlers/scene/SceneSaveToolHandler.js +5 -6
- package/src/handlers/script/CodeIndexBuildToolHandler.js +9 -2
- package/src/handlers/script/ScriptRefsFindToolHandler.js +27 -21
- package/src/handlers/script/ScriptSearchToolHandler.js +7 -13
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +17 -14
- package/src/handlers/ui/UIFindElementsToolHandler.js +14 -2
- package/src/tools/analysis/getComponentValues.js +0 -7
- package/src/tools/scene/getSceneInfo.js +4 -4
package/bin/unity-mcp-server.js
CHANGED
|
@@ -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
|
|
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
package/src/core/config.js
CHANGED
|
@@ -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:
|
|
75
|
-
mcpHost:
|
|
76
|
-
bindHost:
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 (
|
|
164
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
206
|
+
if (telemetryEnabled !== undefined) {
|
|
207
|
+
out.telemetry = { enabled: telemetryEnabled };
|
|
208
|
+
}
|
|
180
209
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const legacyBindHost = unityConfig.bindHost;
|
|
210
|
+
if (lspRequestTimeoutMs !== undefined) {
|
|
211
|
+
out.lsp = { requestTimeoutMs: lspRequestTimeoutMs };
|
|
212
|
+
}
|
|
185
213
|
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
//
|
|
195
|
-
if (
|
|
196
|
-
|
|
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
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
}
|
|
215
|
-
|
|
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
|
);
|
package/src/core/projectInfo.js
CHANGED
|
@@ -46,7 +46,23 @@ export class ProjectInfoProvider {
|
|
|
46
46
|
|
|
47
47
|
async get() {
|
|
48
48
|
if (this.cached) return this.cached;
|
|
49
|
-
//
|
|
49
|
+
// Env-driven project root override (explicit, deterministic)
|
|
50
|
+
const envRootRaw = process.env.UNITY_PROJECT_ROOT;
|
|
51
|
+
if (typeof envRootRaw === 'string' && envRootRaw.trim().length > 0) {
|
|
52
|
+
const envRoot = envRootRaw.trim();
|
|
53
|
+
const projectRoot = normalize(path.resolve(envRoot));
|
|
54
|
+
const codeIndexRoot = normalize(resolveDefaultCodeIndexRoot(projectRoot));
|
|
55
|
+
this.cached = {
|
|
56
|
+
projectRoot,
|
|
57
|
+
assetsPath: normalize(path.join(projectRoot, 'Assets')),
|
|
58
|
+
packagesPath: normalize(path.join(projectRoot, 'Packages')),
|
|
59
|
+
packageCachePath: normalize(path.join(projectRoot, 'Library/PackageCache')),
|
|
60
|
+
codeIndexRoot
|
|
61
|
+
};
|
|
62
|
+
return this.cached;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Config-driven project root (env is mapped into config.project.root)
|
|
50
66
|
const cfgRootRaw = config?.project?.root;
|
|
51
67
|
if (typeof cfgRootRaw === 'string' && cfgRootRaw.trim().length > 0) {
|
|
52
68
|
const cfgRoot = cfgRootRaw.trim();
|
|
@@ -61,6 +77,7 @@ export class ProjectInfoProvider {
|
|
|
61
77
|
projectRoot,
|
|
62
78
|
assetsPath: normalize(path.join(projectRoot, 'Assets')),
|
|
63
79
|
packagesPath: normalize(path.join(projectRoot, 'Packages')),
|
|
80
|
+
packageCachePath: normalize(path.join(projectRoot, 'Library/PackageCache')),
|
|
64
81
|
codeIndexRoot
|
|
65
82
|
};
|
|
66
83
|
return this.cached;
|
|
@@ -76,6 +93,9 @@ export class ProjectInfoProvider {
|
|
|
76
93
|
projectRoot: info.projectRoot,
|
|
77
94
|
assetsPath: info.assetsPath,
|
|
78
95
|
packagesPath: normalize(info.packagesPath || path.join(info.projectRoot, 'Packages')),
|
|
96
|
+
packageCachePath: normalize(
|
|
97
|
+
info.packageCachePath || path.join(info.projectRoot, 'Library/PackageCache')
|
|
98
|
+
),
|
|
79
99
|
codeIndexRoot: normalize(
|
|
80
100
|
info.codeIndexRoot || resolveDefaultCodeIndexRoot(info.projectRoot)
|
|
81
101
|
)
|
|
@@ -96,18 +116,14 @@ export class ProjectInfoProvider {
|
|
|
96
116
|
projectRoot,
|
|
97
117
|
assetsPath: normalize(path.join(projectRoot, 'Assets')),
|
|
98
118
|
packagesPath: normalize(path.join(projectRoot, 'Packages')),
|
|
119
|
+
packageCachePath: normalize(path.join(projectRoot, 'Library/PackageCache')),
|
|
99
120
|
codeIndexRoot
|
|
100
121
|
};
|
|
101
122
|
return this.cached;
|
|
102
123
|
}
|
|
103
124
|
|
|
104
|
-
if (typeof cfgRootRaw === 'string') {
|
|
105
|
-
throw new Error(
|
|
106
|
-
'project.root is configured but empty. Set a valid path in .unity/config.json.'
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
125
|
throw new Error(
|
|
110
|
-
'Unable to resolve Unity project root. Start the server inside a Unity project (directory containing Assets/ and Packages/) or
|
|
126
|
+
'Unable to resolve Unity project root. Start the server inside a Unity project (directory containing Assets/ and Packages/) or set UNITY_PROJECT_ROOT.'
|
|
111
127
|
);
|
|
112
128
|
}
|
|
113
129
|
|
package/src/core/server.js
CHANGED
|
@@ -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');
|
|
@@ -22,6 +22,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
22
22
|
this.inFlight = 0;
|
|
23
23
|
this.maxInFlight = 1; // process one command at a time by default
|
|
24
24
|
this.connectedAt = null; // Timestamp when connection was established
|
|
25
|
+
this.hasConnectedOnce = false;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -86,6 +87,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
86
87
|
this.connected = true;
|
|
87
88
|
this.reconnectAttempts = 0;
|
|
88
89
|
this.connectPromise = null;
|
|
90
|
+
this.hasConnectedOnce = true;
|
|
89
91
|
this.emit('connected');
|
|
90
92
|
this._pumpQueue(); // flush any queued commands that arrived during reconnect
|
|
91
93
|
settle(resolve);
|
|
@@ -120,7 +122,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
120
122
|
// Destroy the socket to clean up properly
|
|
121
123
|
this.socket.destroy();
|
|
122
124
|
this.isDisconnecting = false;
|
|
123
|
-
reject(error);
|
|
125
|
+
reject(wrapUnityConnectError(error, targetHost, config.unity.port));
|
|
124
126
|
} else if (this.connected) {
|
|
125
127
|
// Force close to trigger reconnect logic
|
|
126
128
|
try {
|
|
@@ -175,9 +177,11 @@ export class UnityConnection extends EventEmitter {
|
|
|
175
177
|
this.socket.removeAllListeners();
|
|
176
178
|
this.socket.destroy();
|
|
177
179
|
this.connectPromise = null;
|
|
178
|
-
|
|
180
|
+
const timeoutError = new Error('Connection timeout');
|
|
181
|
+
timeoutError.code = 'ETIMEDOUT';
|
|
182
|
+
settle(reject, wrapUnityConnectError(timeoutError, targetHost, config.unity.port));
|
|
179
183
|
}
|
|
180
|
-
}, config.unity.commandTimeout);
|
|
184
|
+
}, config.unity.connectTimeout ?? config.unity.commandTimeout);
|
|
181
185
|
});
|
|
182
186
|
return this.connectPromise;
|
|
183
187
|
}
|
|
@@ -361,7 +365,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
361
365
|
if (!this.connected) {
|
|
362
366
|
logger.warning('[Unity] Not connected; waiting for reconnection before sending command');
|
|
363
367
|
await this.ensureConnected({
|
|
364
|
-
timeoutMs: config.unity.commandTimeout
|
|
368
|
+
timeoutMs: config.unity.connectTimeout ?? config.unity.commandTimeout
|
|
365
369
|
});
|
|
366
370
|
}
|
|
367
371
|
|
|
@@ -518,6 +522,13 @@ export class UnityConnection extends EventEmitter {
|
|
|
518
522
|
if (/test environment/i.test(msg)) {
|
|
519
523
|
throw error;
|
|
520
524
|
}
|
|
525
|
+
if (
|
|
526
|
+
!this.hasConnectedOnce &&
|
|
527
|
+
(error?.code === 'UNITY_CONNECTION_REFUSED' || error?.code === 'ECONNREFUSED')
|
|
528
|
+
) {
|
|
529
|
+
// Fail fast for the initial connection when Unity isn't listening.
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
521
532
|
}
|
|
522
533
|
|
|
523
534
|
if (this.connected) return;
|
|
@@ -525,8 +536,9 @@ export class UnityConnection extends EventEmitter {
|
|
|
525
536
|
}
|
|
526
537
|
|
|
527
538
|
if (!this.connected) {
|
|
539
|
+
const targetHost = config.unity.mcpHost || config.unity.unityHost || 'localhost';
|
|
528
540
|
const error = new Error(
|
|
529
|
-
`Failed to
|
|
541
|
+
`Failed to connect to Unity at ${targetHost}:${config.unity.port} within ${timeoutMs}ms${lastError ? `: ${lastError.message}` : ''}`
|
|
530
542
|
);
|
|
531
543
|
error.code = 'UNITY_RECONNECT_TIMEOUT';
|
|
532
544
|
throw error;
|
|
@@ -545,3 +557,33 @@ export class UnityConnection extends EventEmitter {
|
|
|
545
557
|
function sleep(ms) {
|
|
546
558
|
return new Promise(resolve => setTimeout(resolve, Math.max(0, ms || 0)));
|
|
547
559
|
}
|
|
560
|
+
|
|
561
|
+
function wrapUnityConnectError(error, host, port) {
|
|
562
|
+
const code = error?.code || '';
|
|
563
|
+
if (code === 'ECONNREFUSED') {
|
|
564
|
+
const hint = buildUnityConnectionHint(host, port);
|
|
565
|
+
const wrapped = new Error(
|
|
566
|
+
`Unity TCP connection refused (ECONNREFUSED) at ${host}:${port}. ${hint}`
|
|
567
|
+
);
|
|
568
|
+
wrapped.code = 'UNITY_CONNECTION_REFUSED';
|
|
569
|
+
wrapped.cause = error;
|
|
570
|
+
return wrapped;
|
|
571
|
+
}
|
|
572
|
+
if (code === 'ETIMEDOUT') {
|
|
573
|
+
const hint = buildUnityConnectionHint(host, port);
|
|
574
|
+
const wrapped = new Error(`Unity TCP Connection timeout at ${host}:${port}. ${hint}`);
|
|
575
|
+
wrapped.code = 'UNITY_CONNECTION_TIMEOUT';
|
|
576
|
+
wrapped.cause = error;
|
|
577
|
+
return wrapped;
|
|
578
|
+
}
|
|
579
|
+
return error;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function buildUnityConnectionHint(_host, _port) {
|
|
583
|
+
const configPath = config.__configPath || '.unity/config.json';
|
|
584
|
+
return (
|
|
585
|
+
`Start Unity Editor and ensure the Unity MCP package is running (TCP listener). ` +
|
|
586
|
+
`Check ${configPath} (unity.mcpHost/unity.port). ` +
|
|
587
|
+
`If using WSL2/Docker → Windows Unity, set unity.mcpHost=host.docker.internal.`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { parentPort, workerData } from 'worker_threads';
|
|
18
|
-
import { spawn } from 'child_process';
|
|
18
|
+
import { spawn, execFile } from 'child_process';
|
|
19
19
|
import fs from 'fs';
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import os from 'os';
|
|
@@ -107,6 +107,111 @@ function walkCs(root, files, seen) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Fast file enumeration using OS native commands (find on Unix, PowerShell on Windows)
|
|
112
|
+
* Falls back to walkCs on failure
|
|
113
|
+
* @param {string[]} roots - Array of root directories to scan
|
|
114
|
+
* @returns {Promise<string[]>} Array of absolute C# file paths
|
|
115
|
+
*/
|
|
116
|
+
async function fastWalkCs(roots) {
|
|
117
|
+
const isWindows = process.platform === 'win32';
|
|
118
|
+
const allFiles = [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
|
|
121
|
+
for (const root of roots) {
|
|
122
|
+
if (!fs.existsSync(root)) {
|
|
123
|
+
log('info', `[worker] Skipping non-existent root: ${root}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
log('info', `[worker] Scanning ${path.basename(root)}...`);
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const files = await new Promise((resolve, reject) => {
|
|
132
|
+
if (isWindows) {
|
|
133
|
+
// Windows: Use PowerShell (secure - no shell interpretation)
|
|
134
|
+
execFile(
|
|
135
|
+
'powershell',
|
|
136
|
+
[
|
|
137
|
+
'-NoProfile',
|
|
138
|
+
'-Command',
|
|
139
|
+
`Get-ChildItem -Path '${root.replace(/'/g, "''")}' -Recurse -Include *.cs -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName`
|
|
140
|
+
],
|
|
141
|
+
{ maxBuffer: 50 * 1024 * 1024 }, // 50MB buffer for large projects
|
|
142
|
+
(error, stdout) => {
|
|
143
|
+
if (error) {
|
|
144
|
+
reject(error);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const files = stdout
|
|
148
|
+
.split('\r\n')
|
|
149
|
+
.map(f => f.trim())
|
|
150
|
+
.filter(f => f && f.endsWith('.cs'));
|
|
151
|
+
resolve(files);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
// Unix: Use find command (secure - no shell interpretation)
|
|
156
|
+
// Note: We intentionally don't exclude hidden directories (*/.*) because:
|
|
157
|
+
// - The search root may be inside .worktrees which contains a dot
|
|
158
|
+
// - -name '*.cs' already filters to only C# files (excluding .meta etc.)
|
|
159
|
+
execFile(
|
|
160
|
+
'find',
|
|
161
|
+
[
|
|
162
|
+
root,
|
|
163
|
+
'-name',
|
|
164
|
+
'*.cs',
|
|
165
|
+
'-type',
|
|
166
|
+
'f',
|
|
167
|
+
'-not',
|
|
168
|
+
'-path',
|
|
169
|
+
'*/obj/*',
|
|
170
|
+
'-not',
|
|
171
|
+
'-path',
|
|
172
|
+
'*/bin/*'
|
|
173
|
+
],
|
|
174
|
+
{ maxBuffer: 50 * 1024 * 1024 }, // 50MB buffer for large projects
|
|
175
|
+
(error, stdout) => {
|
|
176
|
+
if (error) {
|
|
177
|
+
reject(error);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const files = stdout
|
|
181
|
+
.split('\n')
|
|
182
|
+
.map(f => f.trim())
|
|
183
|
+
.filter(f => f && f.endsWith('.cs'));
|
|
184
|
+
resolve(files);
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const elapsed = Date.now() - startTime;
|
|
191
|
+
log('info', `[worker] Found ${files.length} files in ${path.basename(root)} (${elapsed}ms)`);
|
|
192
|
+
|
|
193
|
+
// Add unique files
|
|
194
|
+
for (const f of files) {
|
|
195
|
+
if (!seen.has(f)) {
|
|
196
|
+
seen.add(f);
|
|
197
|
+
allFiles.push(f);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// Fallback to walkCs on error
|
|
202
|
+
log(
|
|
203
|
+
'warn',
|
|
204
|
+
`[worker] Fast scan failed for ${path.basename(root)}: ${e.message}. Using fallback.`
|
|
205
|
+
);
|
|
206
|
+
const fallbackFiles = [];
|
|
207
|
+
walkCs(root, fallbackFiles, seen);
|
|
208
|
+
allFiles.push(...fallbackFiles);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return allFiles;
|
|
213
|
+
}
|
|
214
|
+
|
|
110
215
|
/**
|
|
111
216
|
* Convert absolute path to relative path
|
|
112
217
|
*/
|
|
@@ -406,7 +511,9 @@ async function runBuild() {
|
|
|
406
511
|
concurrency: _concurrency = 1, // Reserved for future parallel processing
|
|
407
512
|
throttleMs = 0,
|
|
408
513
|
retry = 2,
|
|
409
|
-
reportPercentage = 10
|
|
514
|
+
reportPercentage = 10,
|
|
515
|
+
excludePackageCache = false,
|
|
516
|
+
forceRebuild = false // Skip change detection and rebuild all files
|
|
410
517
|
} = workerData;
|
|
411
518
|
void _concurrency; // Explicitly mark as intentionally unused
|
|
412
519
|
|
|
@@ -473,36 +580,72 @@ async function runBuild() {
|
|
|
473
580
|
runSQL(db, 'CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path)');
|
|
474
581
|
|
|
475
582
|
// Scan for C# files
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
path.resolve(projectRoot, 'Library/PackageCache')
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
583
|
+
// Build roots list based on excludePackageCache option
|
|
584
|
+
const roots = [path.resolve(projectRoot, 'Assets'), path.resolve(projectRoot, 'Packages')];
|
|
585
|
+
if (!excludePackageCache) {
|
|
586
|
+
roots.push(path.resolve(projectRoot, 'Library/PackageCache'));
|
|
587
|
+
}
|
|
588
|
+
log(
|
|
589
|
+
'info',
|
|
590
|
+
`[worker] Scanning roots: ${roots.map(r => path.basename(r)).join(', ')}${excludePackageCache ? ' (PackageCache excluded)' : ''}`
|
|
591
|
+
);
|
|
484
592
|
|
|
593
|
+
// Use fast find command when available, fallback to walkCs
|
|
594
|
+
const files = await fastWalkCs(roots);
|
|
485
595
|
log('info', `[worker] Found ${files.length} C# files to process`);
|
|
486
596
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
597
|
+
let changed = [];
|
|
598
|
+
let removed = [];
|
|
599
|
+
const wanted = new Map();
|
|
600
|
+
|
|
601
|
+
if (forceRebuild) {
|
|
602
|
+
// Skip change detection - process all files
|
|
603
|
+
log('info', `[worker] forceRebuild=true, skipping change detection`);
|
|
604
|
+
// Clear all existing data
|
|
605
|
+
runSQL(db, 'DELETE FROM symbols');
|
|
606
|
+
runSQL(db, 'DELETE FROM files');
|
|
607
|
+
// Mark all files as changed
|
|
608
|
+
for (const abs of files) {
|
|
609
|
+
const rel = toRel(abs, projectRoot);
|
|
610
|
+
wanted.set(rel, '0-0'); // Dummy signature for forceRebuild
|
|
611
|
+
changed.push(rel);
|
|
612
|
+
}
|
|
613
|
+
log('info', `[worker] All ${changed.length} files marked for processing`);
|
|
614
|
+
} else {
|
|
615
|
+
// Normal change detection
|
|
616
|
+
// Get current indexed files
|
|
617
|
+
const currentResult = querySQL(db, 'SELECT path, sig FROM files');
|
|
618
|
+
const current = new Map();
|
|
619
|
+
if (currentResult.length > 0) {
|
|
620
|
+
for (const row of currentResult[0].values) {
|
|
621
|
+
current.set(row[0], row[1]);
|
|
622
|
+
}
|
|
493
623
|
}
|
|
494
|
-
}
|
|
495
624
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
625
|
+
// Determine changes (this calls makeSig for each file)
|
|
626
|
+
log('info', `[worker] Computing file signatures (${files.length} files)...`);
|
|
627
|
+
const sigStartTime = Date.now();
|
|
628
|
+
let sigProcessed = 0;
|
|
629
|
+
for (const abs of files) {
|
|
630
|
+
const rel = toRel(abs, projectRoot);
|
|
631
|
+
const sig = makeSig(abs);
|
|
632
|
+
wanted.set(rel, sig);
|
|
633
|
+
sigProcessed++;
|
|
634
|
+
// Report progress every 10000 files
|
|
635
|
+
if (sigProcessed % 10000 === 0) {
|
|
636
|
+
const elapsed = ((Date.now() - sigStartTime) / 1000).toFixed(1);
|
|
637
|
+
log('info', `[worker] Signature progress: ${sigProcessed}/${files.length} (${elapsed}s)`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const sigTime = ((Date.now() - sigStartTime) / 1000).toFixed(1);
|
|
641
|
+
log('info', `[worker] Signatures computed in ${sigTime}s`);
|
|
500
642
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
643
|
+
for (const [rel, sig] of wanted) {
|
|
644
|
+
if (current.get(rel) !== sig) changed.push(rel);
|
|
645
|
+
}
|
|
646
|
+
for (const [rel] of current) {
|
|
647
|
+
if (!wanted.has(rel)) removed.push(rel);
|
|
648
|
+
}
|
|
506
649
|
}
|
|
507
650
|
|
|
508
651
|
log('info', `[worker] Changes: ${changed.length} to update, ${removed.length} to remove`);
|
|
@@ -64,17 +64,16 @@ export class SceneCreateToolHandler extends BaseToolHandler {
|
|
|
64
64
|
// Send command to Unity
|
|
65
65
|
const result = await this.unityConnection.sendCommand('create_scene', params);
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
if (result
|
|
67
|
+
// Unity returns errors as { error: "..." } payloads (still wrapped in a success response)
|
|
68
|
+
if (result && result.error) {
|
|
69
69
|
const error = new Error(result.error);
|
|
70
70
|
error.code = 'UNITY_ERROR';
|
|
71
71
|
throw error;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// Handle undefined or null results from Unity
|
|
75
|
-
if (result
|
|
74
|
+
// Handle undefined or null results from Unity (best-effort)
|
|
75
|
+
if (result === undefined || result === null) {
|
|
76
76
|
return {
|
|
77
|
-
status: 'success',
|
|
78
77
|
sceneName: params.sceneName,
|
|
79
78
|
path: params.path || 'Assets/Scenes/',
|
|
80
79
|
loadScene: params.loadScene !== false,
|
|
@@ -83,6 +82,6 @@ export class SceneCreateToolHandler extends BaseToolHandler {
|
|
|
83
82
|
};
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
return result
|
|
85
|
+
return result;
|
|
87
86
|
}
|
|
88
87
|
}
|
|
@@ -67,17 +67,16 @@ export class SceneLoadToolHandler extends BaseToolHandler {
|
|
|
67
67
|
// Send command to Unity
|
|
68
68
|
const result = await this.unityConnection.sendCommand('load_scene', params);
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
if (result
|
|
70
|
+
// Unity returns errors as { error: "..." } payloads (still wrapped in a success response)
|
|
71
|
+
if (result && result.error) {
|
|
72
72
|
const error = new Error(result.error);
|
|
73
73
|
error.code = 'UNITY_ERROR';
|
|
74
74
|
throw error;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Handle undefined or null results from Unity
|
|
78
|
-
if (result
|
|
77
|
+
// Handle undefined or null results from Unity (best-effort)
|
|
78
|
+
if (result === undefined || result === null) {
|
|
79
79
|
return {
|
|
80
|
-
status: 'success',
|
|
81
80
|
sceneName: params.sceneName || 'Unknown',
|
|
82
81
|
scenePath: params.scenePath || 'Unknown',
|
|
83
82
|
loadMode: params.loadMode || 'Single',
|
|
@@ -85,6 +84,6 @@ export class SceneLoadToolHandler extends BaseToolHandler {
|
|
|
85
84
|
};
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
return result
|
|
87
|
+
return result;
|
|
89
88
|
}
|
|
90
89
|
}
|
|
@@ -51,23 +51,22 @@ export class SceneSaveToolHandler extends BaseToolHandler {
|
|
|
51
51
|
// Send command to Unity
|
|
52
52
|
const result = await this.unityConnection.sendCommand('save_scene', params);
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
if (result
|
|
54
|
+
// Unity returns errors as { error: "..." } payloads (still wrapped in a success response)
|
|
55
|
+
if (result && result.error) {
|
|
56
56
|
const error = new Error(result.error);
|
|
57
57
|
error.code = 'UNITY_ERROR';
|
|
58
58
|
throw error;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Handle undefined or null results from Unity
|
|
62
|
-
if (result
|
|
61
|
+
// Handle undefined or null results from Unity (best-effort)
|
|
62
|
+
if (result === undefined || result === null) {
|
|
63
63
|
return {
|
|
64
|
-
status: 'success',
|
|
65
64
|
scenePath: params.scenePath || 'Current scene path',
|
|
66
65
|
saveAs: params.saveAs === true,
|
|
67
66
|
message: 'Scene save completed but Unity returned no details'
|
|
68
67
|
};
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
return result
|
|
70
|
+
return result;
|
|
72
71
|
}
|
|
73
72
|
}
|
|
@@ -23,6 +23,11 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
|
|
|
23
23
|
minimum: 0,
|
|
24
24
|
maximum: 5,
|
|
25
25
|
description: 'Number of retries for LSP requests (default: 2).'
|
|
26
|
+
},
|
|
27
|
+
excludePackageCache: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description:
|
|
30
|
+
'Exclude Library/PackageCache from indexing (default: false). Set to true for faster builds when package symbols are not needed.'
|
|
26
31
|
}
|
|
27
32
|
},
|
|
28
33
|
required: []
|
|
@@ -54,7 +59,8 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
|
|
|
54
59
|
return {
|
|
55
60
|
success: false,
|
|
56
61
|
error: 'build_already_running',
|
|
57
|
-
message:
|
|
62
|
+
message:
|
|
63
|
+
'Code index build is already running (Worker Thread). Use code_index_status to check progress.',
|
|
58
64
|
jobId: this.currentJobId
|
|
59
65
|
};
|
|
60
66
|
}
|
|
@@ -86,7 +92,8 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
|
|
|
86
92
|
concurrency: 1, // Worker Thread uses sequential processing for stability
|
|
87
93
|
throttleMs: Math.max(0, Number(params?.throttleMs ?? 0)),
|
|
88
94
|
retry: Math.max(0, Math.min(5, Number(params?.retry ?? 2))),
|
|
89
|
-
reportPercentage: 10
|
|
95
|
+
reportPercentage: 10,
|
|
96
|
+
excludePackageCache: Boolean(params?.excludePackageCache)
|
|
90
97
|
});
|
|
91
98
|
|
|
92
99
|
// Clear current job tracking on success
|
|
@@ -213,42 +213,48 @@ export class ScriptRefsFindToolHandler extends BaseToolHandler {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
// Phase 3.
|
|
217
|
-
|
|
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
|
|
224
|
+
const references = [];
|
|
229
225
|
for (const it of arr) {
|
|
230
|
-
|
|
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
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
bytes +=
|
|
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
|
-
|
|
253
|
+
if (extremeLimits && results.length === 0) {
|
|
254
|
+
truncated = true;
|
|
255
|
+
}
|
|
250
256
|
|
|
251
|
-
return { success: true,
|
|
257
|
+
return { success: true, results, total, truncated };
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
|
|
@@ -8,7 +8,7 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
|
|
|
8
8
|
constructor(unityConnection) {
|
|
9
9
|
super(
|
|
10
10
|
'script_search',
|
|
11
|
-
'[OFFLINE] No Unity connection required. Search C# by substring/regex/glob with pagination and snippet context. PRIORITY: Use to locate symbols/files; avoid full contents. Use returnMode="snippets" (or "metadata") with small snippetContext (1–2). Narrow aggressively via include globs under Assets
|
|
11
|
+
'[OFFLINE] No Unity connection required. Search C# by substring/regex/glob with pagination and snippet context. PRIORITY: Use to locate symbols/files; avoid full contents. Use returnMode="snippets" (or "metadata") with small snippetContext (1–2). Narrow aggressively via include globs under Assets/**, Packages/**, or Library/PackageCache/** and semantic filters (namespace/container/identifier). Do NOT prefix repository folders.',
|
|
12
12
|
{
|
|
13
13
|
type: 'object',
|
|
14
14
|
properties: {
|
|
@@ -29,9 +29,9 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
|
|
|
29
29
|
},
|
|
30
30
|
scope: {
|
|
31
31
|
type: 'string',
|
|
32
|
-
enum: ['assets', 'packages', 'embedded', 'all'],
|
|
32
|
+
enum: ['assets', 'packages', 'embedded', 'library', 'all'],
|
|
33
33
|
description:
|
|
34
|
-
'Search scope: assets (Assets
|
|
34
|
+
'Search scope: assets (Assets/), packages (Packages/), embedded, library (Library/PackageCache/), or all. Default: all.'
|
|
35
35
|
},
|
|
36
36
|
include: {
|
|
37
37
|
type: 'string',
|
|
@@ -151,6 +151,7 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
|
|
|
151
151
|
if (scope === 'assets' || scope === 'all') roots.push(info.assetsPath);
|
|
152
152
|
if (scope === 'packages' || scope === 'embedded' || scope === 'all')
|
|
153
153
|
roots.push(info.packagesPath);
|
|
154
|
+
if (scope === 'library' || scope === 'all') roots.push(info.packageCachePath);
|
|
154
155
|
|
|
155
156
|
const includeRx = globToRegExp(include);
|
|
156
157
|
const excludeRx = exclude ? globToRegExp(exclude) : null;
|
|
@@ -160,9 +161,8 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
|
|
|
160
161
|
}
|
|
161
162
|
const matcher = buildMatcher(patternType, pattern, flags);
|
|
162
163
|
|
|
164
|
+
// Phase 3.3: Grouped output format (more compact, LLM-friendly)
|
|
163
165
|
const results = [];
|
|
164
|
-
const pathTable = [];
|
|
165
|
-
const pathId = new Map();
|
|
166
166
|
let bytes = 0;
|
|
167
167
|
let afterFound = !startAfter;
|
|
168
168
|
|
|
@@ -199,13 +199,8 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
|
|
|
199
199
|
}
|
|
200
200
|
if (matches === 0) continue;
|
|
201
201
|
|
|
202
|
-
const id = pathId.has(rel)
|
|
203
|
-
? pathId.get(rel)
|
|
204
|
-
: (pathTable.push(rel) - 1, pathTable.length - 1);
|
|
205
|
-
pathId.set(rel, id);
|
|
206
|
-
|
|
207
202
|
const lineRanges = toRanges(matchedLines);
|
|
208
|
-
const item = {
|
|
203
|
+
const item = { path: rel, lineRanges };
|
|
209
204
|
|
|
210
205
|
if (normalizedReturnMode === 'snippets') {
|
|
211
206
|
// Build minimal snippets around first few matches
|
|
@@ -228,10 +223,9 @@ export class ScriptSearchToolHandler extends BaseToolHandler {
|
|
|
228
223
|
return {
|
|
229
224
|
success: true,
|
|
230
225
|
total: results.length,
|
|
231
|
-
pathTable,
|
|
232
226
|
results,
|
|
233
227
|
cursor:
|
|
234
|
-
results.length && results.length >= pageSize ?
|
|
228
|
+
results.length && results.length >= pageSize ? results[results.length - 1].path : null
|
|
235
229
|
};
|
|
236
230
|
} catch (e) {
|
|
237
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:
|
|
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.
|
|
114
|
-
//
|
|
115
|
-
const
|
|
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
|
-
|
|
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,
|
|
141
|
+
return { success: true, results, total };
|
|
139
142
|
}
|
|
140
143
|
}
|
|
@@ -24,6 +24,10 @@ export class UIFindElementsToolHandler extends BaseToolHandler {
|
|
|
24
24
|
canvasFilter: {
|
|
25
25
|
type: 'string',
|
|
26
26
|
description: 'Filter by parent Canvas name.'
|
|
27
|
+
},
|
|
28
|
+
uiSystem: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Filter by UI system: ugui|uitk|imgui|all (default: all).'
|
|
27
31
|
}
|
|
28
32
|
},
|
|
29
33
|
required: []
|
|
@@ -32,7 +36,14 @@ export class UIFindElementsToolHandler extends BaseToolHandler {
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
async execute(params = {}) {
|
|
35
|
-
const {
|
|
39
|
+
const {
|
|
40
|
+
elementType,
|
|
41
|
+
tagFilter,
|
|
42
|
+
namePattern,
|
|
43
|
+
includeInactive = false,
|
|
44
|
+
canvasFilter,
|
|
45
|
+
uiSystem
|
|
46
|
+
} = params;
|
|
36
47
|
|
|
37
48
|
// Ensure connected
|
|
38
49
|
if (!this.unityConnection.isConnected()) {
|
|
@@ -44,7 +55,8 @@ export class UIFindElementsToolHandler extends BaseToolHandler {
|
|
|
44
55
|
tagFilter,
|
|
45
56
|
namePattern,
|
|
46
57
|
includeInactive,
|
|
47
|
-
canvasFilter
|
|
58
|
+
canvasFilter,
|
|
59
|
+
uiSystem
|
|
48
60
|
});
|
|
49
61
|
|
|
50
62
|
// Return the result for the BaseToolHandler to format
|
|
@@ -106,14 +106,10 @@ export async function getComponentValuesHandler(unityConnection, args) {
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// Success response - result is already the unwrapped data
|
|
110
|
-
console.log('[DEBUG] GetComponentValues - Full result:', JSON.stringify(result, null, 2));
|
|
111
|
-
|
|
112
109
|
let responseText = result.summary || `Component values retrieved`;
|
|
113
110
|
|
|
114
111
|
// Add detailed property information if available
|
|
115
112
|
if (result.properties && Object.keys(result.properties).length > 0) {
|
|
116
|
-
console.log('[DEBUG] Properties found:', Object.keys(result.properties).length);
|
|
117
113
|
responseText += '\n\nProperties:';
|
|
118
114
|
for (const [key, value] of Object.entries(result.properties)) {
|
|
119
115
|
if (value && typeof value === 'object') {
|
|
@@ -144,9 +140,6 @@ export async function getComponentValuesHandler(unityConnection, args) {
|
|
|
144
140
|
}
|
|
145
141
|
}
|
|
146
142
|
}
|
|
147
|
-
} else {
|
|
148
|
-
console.log('[DEBUG] No properties found in result');
|
|
149
|
-
console.log('[DEBUG] Result keys:', Object.keys(result));
|
|
150
143
|
}
|
|
151
144
|
|
|
152
145
|
// Add debug information if available
|
|
@@ -57,10 +57,10 @@ export async function getSceneInfoHandler(unityConnection, args) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Send command to Unity
|
|
60
|
-
const result = await unityConnection.sendCommand('
|
|
60
|
+
const result = await unityConnection.sendCommand('get_scene_info', args);
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
if (result
|
|
62
|
+
// Unity returns errors as { error: "..." } payloads (still wrapped in a success response)
|
|
63
|
+
if (result && result.error) {
|
|
64
64
|
return {
|
|
65
65
|
content: [
|
|
66
66
|
{
|
|
@@ -77,7 +77,7 @@ export async function getSceneInfoHandler(unityConnection, args) {
|
|
|
77
77
|
content: [
|
|
78
78
|
{
|
|
79
79
|
type: 'text',
|
|
80
|
-
text: result
|
|
80
|
+
text: result?.summary || `Scene information retrieved`
|
|
81
81
|
}
|
|
82
82
|
],
|
|
83
83
|
isError: false
|