@akiojin/unity-mcp-server 5.3.2 → 5.5.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/package.json +2 -2
- package/scripts/ensure-better-sqlite3.mjs +2 -1
- package/src/core/config.js +63 -2
- package/src/core/httpServer.js +16 -1
- package/src/core/indexProgress.js +15 -0
- package/src/core/projectRootGuard.js +48 -0
- package/src/core/server.js +75 -4
- package/src/core/toolCategoryFilter.js +309 -0
- package/src/core/toolManifest.json +8 -0
- package/src/core/workers/indexBuildWorker.js +14 -4
- package/src/handlers/index.js +2 -0
- package/src/handlers/script/CodeIndexStatusToolHandler.js +52 -2
- package/src/handlers/script/ScriptEditSnippetToolHandler.js +47 -19
- package/src/handlers/system/SystemGetServerInfoToolHandler.js +55 -0
- package/src/lsp/LspRpcClient.js +17 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akiojin/unity-mcp-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lru-cache": "^11.0.2"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=18 <
|
|
31
|
+
"node": ">=18 <25"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/core/config.js
CHANGED
|
@@ -52,6 +52,15 @@ function parseIntEnv(value) {
|
|
|
52
52
|
return Number.isFinite(n) ? n : undefined;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function parseCsvEnv(value) {
|
|
56
|
+
if (typeof value !== 'string') return undefined;
|
|
57
|
+
const items = value
|
|
58
|
+
.split(',')
|
|
59
|
+
.map(s => s.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
return items;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
function envString(key) {
|
|
56
65
|
const raw = process.env[key];
|
|
57
66
|
if (typeof raw !== 'string') return undefined;
|
|
@@ -105,7 +114,8 @@ const baseConfig = {
|
|
|
105
114
|
// Project settings (primarily for code index paths)
|
|
106
115
|
project: {
|
|
107
116
|
root: null,
|
|
108
|
-
codeIndexRoot: null
|
|
117
|
+
codeIndexRoot: null,
|
|
118
|
+
requireClientRoot: false
|
|
109
119
|
},
|
|
110
120
|
|
|
111
121
|
// Server settings
|
|
@@ -142,6 +152,12 @@ const baseConfig = {
|
|
|
142
152
|
fields: []
|
|
143
153
|
},
|
|
144
154
|
|
|
155
|
+
// Tool visibility filter
|
|
156
|
+
tools: {
|
|
157
|
+
includeCategories: [],
|
|
158
|
+
excludeCategories: []
|
|
159
|
+
},
|
|
160
|
+
|
|
145
161
|
// Write queue removed: all edits go through structured Roslyn tools.
|
|
146
162
|
|
|
147
163
|
// Search-related defaults and engine selection
|
|
@@ -154,7 +170,8 @@ const baseConfig = {
|
|
|
154
170
|
// LSP client defaults
|
|
155
171
|
lsp: {
|
|
156
172
|
requestTimeoutMs: 120000,
|
|
157
|
-
slowRequestWarnMs: 2000
|
|
173
|
+
slowRequestWarnMs: 2000,
|
|
174
|
+
validationTimeoutMs: 5000
|
|
158
175
|
},
|
|
159
176
|
|
|
160
177
|
// Indexing (code index) settings
|
|
@@ -176,6 +193,7 @@ function loadEnvConfig() {
|
|
|
176
193
|
const unityPort = parseIntEnv(process.env.UNITY_MCP_PORT);
|
|
177
194
|
|
|
178
195
|
const projectRoot = envString('UNITY_PROJECT_ROOT');
|
|
196
|
+
const requireProjectRoot = parseBoolEnv(process.env.UNITY_MCP_REQUIRE_PROJECT_ROOT);
|
|
179
197
|
|
|
180
198
|
const logLevel = envString('UNITY_MCP_LOG_LEVEL');
|
|
181
199
|
const versionMismatch = envString('UNITY_MCP_VERSION_MISMATCH');
|
|
@@ -186,6 +204,9 @@ function loadEnvConfig() {
|
|
|
186
204
|
const telemetryEnabled = parseBoolEnv(process.env.UNITY_MCP_TELEMETRY_ENABLED);
|
|
187
205
|
const lspRequestTimeoutMs = parseIntEnv(process.env.UNITY_MCP_LSP_REQUEST_TIMEOUT_MS);
|
|
188
206
|
const lspSlowRequestWarnMs = parseIntEnv(process.env.UNITY_MCP_LSP_SLOW_REQUEST_WARN_MS);
|
|
207
|
+
const lspValidationTimeoutMs = parseIntEnv(process.env.UNITY_MCP_LSP_VALIDATION_TIMEOUT_MS);
|
|
208
|
+
const includeCategories = parseCsvEnv(process.env.UNITY_MCP_TOOL_INCLUDE_CATEGORIES);
|
|
209
|
+
const excludeCategories = parseCsvEnv(process.env.UNITY_MCP_TOOL_EXCLUDE_CATEGORIES);
|
|
189
210
|
|
|
190
211
|
const out = {};
|
|
191
212
|
|
|
@@ -201,6 +222,9 @@ function loadEnvConfig() {
|
|
|
201
222
|
out.project = {};
|
|
202
223
|
if (projectRoot) out.project.root = projectRoot;
|
|
203
224
|
}
|
|
225
|
+
if (requireProjectRoot !== undefined) {
|
|
226
|
+
out.project = { ...(out.project || {}), requireClientRoot: requireProjectRoot };
|
|
227
|
+
}
|
|
204
228
|
|
|
205
229
|
if (logLevel) {
|
|
206
230
|
out.logging = { level: logLevel };
|
|
@@ -220,12 +244,21 @@ function loadEnvConfig() {
|
|
|
220
244
|
out.telemetry = { enabled: telemetryEnabled };
|
|
221
245
|
}
|
|
222
246
|
|
|
247
|
+
if (includeCategories !== undefined || excludeCategories !== undefined) {
|
|
248
|
+
out.tools = {};
|
|
249
|
+
if (includeCategories !== undefined) out.tools.includeCategories = includeCategories;
|
|
250
|
+
if (excludeCategories !== undefined) out.tools.excludeCategories = excludeCategories;
|
|
251
|
+
}
|
|
252
|
+
|
|
223
253
|
if (lspRequestTimeoutMs !== undefined) {
|
|
224
254
|
out.lsp = { requestTimeoutMs: lspRequestTimeoutMs };
|
|
225
255
|
}
|
|
226
256
|
if (lspSlowRequestWarnMs !== undefined) {
|
|
227
257
|
out.lsp = { ...(out.lsp || {}), slowRequestWarnMs: lspSlowRequestWarnMs };
|
|
228
258
|
}
|
|
259
|
+
if (lspValidationTimeoutMs !== undefined) {
|
|
260
|
+
out.lsp = { ...(out.lsp || {}), validationTimeoutMs: lspValidationTimeoutMs };
|
|
261
|
+
}
|
|
229
262
|
|
|
230
263
|
return out;
|
|
231
264
|
}
|
|
@@ -296,6 +329,25 @@ function validateAndNormalizeConfig(cfg) {
|
|
|
296
329
|
cfg.project.codeIndexRoot = null;
|
|
297
330
|
}
|
|
298
331
|
|
|
332
|
+
if (!cfg.tools || typeof cfg.tools !== 'object') {
|
|
333
|
+
cfg.tools = {};
|
|
334
|
+
}
|
|
335
|
+
if (!Array.isArray(cfg.tools.includeCategories)) {
|
|
336
|
+
cfg.tools.includeCategories = [];
|
|
337
|
+
}
|
|
338
|
+
if (!Array.isArray(cfg.tools.excludeCategories)) {
|
|
339
|
+
cfg.tools.excludeCategories = [];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
cfg.tools.includeCategories = cfg.tools.includeCategories
|
|
343
|
+
.filter(v => typeof v === 'string')
|
|
344
|
+
.map(v => v.trim())
|
|
345
|
+
.filter(Boolean);
|
|
346
|
+
cfg.tools.excludeCategories = cfg.tools.excludeCategories
|
|
347
|
+
.filter(v => typeof v === 'string')
|
|
348
|
+
.map(v => v.trim())
|
|
349
|
+
.filter(Boolean);
|
|
350
|
+
|
|
299
351
|
// lsp timeout sanity
|
|
300
352
|
if (cfg.lsp?.requestTimeoutMs !== undefined) {
|
|
301
353
|
const t = Number(cfg.lsp.requestTimeoutMs);
|
|
@@ -315,6 +367,15 @@ function validateAndNormalizeConfig(cfg) {
|
|
|
315
367
|
cfg.lsp.slowRequestWarnMs = 2000;
|
|
316
368
|
}
|
|
317
369
|
}
|
|
370
|
+
if (cfg.lsp?.validationTimeoutMs !== undefined) {
|
|
371
|
+
const t = Number(cfg.lsp.validationTimeoutMs);
|
|
372
|
+
if (!Number.isFinite(t) || t < 0) {
|
|
373
|
+
logger.warning(
|
|
374
|
+
`[unity-mcp-server] WARN: Invalid UNITY_MCP_LSP_VALIDATION_TIMEOUT_MS (${cfg.lsp.validationTimeoutMs}); using default 5000`
|
|
375
|
+
);
|
|
376
|
+
cfg.lsp.validationTimeoutMs = 5000;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
318
379
|
}
|
|
319
380
|
|
|
320
381
|
export const config = merge(baseConfig, loadEnvConfig());
|
package/src/core/httpServer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { logger } from './config.js';
|
|
3
|
+
import { createProjectRootGuard } from './projectRootGuard.js';
|
|
3
4
|
|
|
4
5
|
function buildHealthResponse({ startedAt, mode, port, telemetryEnabled }) {
|
|
5
6
|
return {
|
|
@@ -22,10 +23,12 @@ export function createHttpServer({
|
|
|
22
23
|
port = 6401,
|
|
23
24
|
telemetryEnabled = false,
|
|
24
25
|
healthPath = '/healthz',
|
|
25
|
-
allowedHosts = ['localhost', '127.0.0.1']
|
|
26
|
+
allowedHosts = ['localhost', '127.0.0.1'],
|
|
27
|
+
requireClientRoot = false
|
|
26
28
|
} = {}) {
|
|
27
29
|
const startedAt = Date.now();
|
|
28
30
|
let server;
|
|
31
|
+
const projectRootGuard = createProjectRootGuard({ requireClientRoot, logger });
|
|
29
32
|
|
|
30
33
|
const listener = async (req, res) => {
|
|
31
34
|
try {
|
|
@@ -76,6 +79,18 @@ export function createHttpServer({
|
|
|
76
79
|
if (method === 'tools/call' || method === 'callTool') {
|
|
77
80
|
const name = params?.name;
|
|
78
81
|
const args = params?.arguments || {};
|
|
82
|
+
const guardError = await projectRootGuard(args);
|
|
83
|
+
if (guardError) {
|
|
84
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
85
|
+
res.end(
|
|
86
|
+
JSON.stringify({
|
|
87
|
+
jsonrpc: '2.0',
|
|
88
|
+
id,
|
|
89
|
+
error: { code: -32010, message: guardError }
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
79
94
|
const handler = handlers.get(name);
|
|
80
95
|
if (!handler) {
|
|
81
96
|
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function buildProgress({ phase, processed = 0, total = 0, rate = 0 } = {}) {
|
|
2
|
+
const safePhase = phase && String(phase).trim() ? String(phase) : 'index';
|
|
3
|
+
return {
|
|
4
|
+
phase: safePhase,
|
|
5
|
+
processed: Number.isFinite(processed) ? processed : 0,
|
|
6
|
+
total: Number.isFinite(total) ? total : 0,
|
|
7
|
+
rate: Number.isFinite(rate) ? rate : 0
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getReportEvery(total, steps = 20) {
|
|
12
|
+
const safeTotal = Number.isFinite(total) && total > 0 ? total : 0;
|
|
13
|
+
const safeSteps = Number.isFinite(steps) && steps > 0 ? steps : 20;
|
|
14
|
+
return Math.max(1, Math.floor(safeTotal / safeSteps) || 1);
|
|
15
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ProjectInfoProvider } from './projectInfo.js';
|
|
3
|
+
|
|
4
|
+
const normalizeRoot = root => {
|
|
5
|
+
if (!root) return '';
|
|
6
|
+
const resolved = path.resolve(String(root));
|
|
7
|
+
const normalized = resolved.replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
8
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createProjectRootGuard({
|
|
12
|
+
requireClientRoot = false,
|
|
13
|
+
projectInfoProvider = null,
|
|
14
|
+
logger = null
|
|
15
|
+
} = {}) {
|
|
16
|
+
const provider = projectInfoProvider || new ProjectInfoProvider();
|
|
17
|
+
let cachedRoot = null;
|
|
18
|
+
|
|
19
|
+
const getServerRoot = async () => {
|
|
20
|
+
if (cachedRoot) return cachedRoot;
|
|
21
|
+
try {
|
|
22
|
+
const info = await provider.get();
|
|
23
|
+
cachedRoot = normalizeRoot(info?.projectRoot || '');
|
|
24
|
+
} catch (e) {
|
|
25
|
+
logger?.warning?.(`[unity-mcp-server] project root resolve failed: ${e.message}`);
|
|
26
|
+
}
|
|
27
|
+
return cachedRoot;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return async function guard(args = {}) {
|
|
31
|
+
const clientRootRaw = args?.projectRoot;
|
|
32
|
+
if (!requireClientRoot && !clientRootRaw) return null;
|
|
33
|
+
|
|
34
|
+
if (!clientRootRaw) {
|
|
35
|
+
return 'projectRoot is required. Call get_server_info and pass projectRoot with tool arguments.';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const clientRoot = normalizeRoot(clientRootRaw);
|
|
39
|
+
const serverRoot = await getServerRoot();
|
|
40
|
+
if (!serverRoot) {
|
|
41
|
+
return 'server projectRoot could not be resolved';
|
|
42
|
+
}
|
|
43
|
+
if (clientRoot !== serverRoot) {
|
|
44
|
+
return `projectRoot mismatch (client=${clientRootRaw}, server=${serverRoot})`;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/core/server.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import { StdioRpcServer } from './stdioRpcServer.js';
|
|
18
|
+
import { createProjectRootGuard } from './projectRootGuard.js';
|
|
19
|
+
import { createToolCategoryPolicy, filterToolsByCategory } from './toolCategoryFilter.js';
|
|
18
20
|
|
|
19
21
|
// Deferred state - will be initialized after transport connection
|
|
20
22
|
let unityConnection = null;
|
|
@@ -125,8 +127,50 @@ export async function startServer(options = {}) {
|
|
|
125
127
|
...config,
|
|
126
128
|
http: { ...config.http, ...(options.http || {}) },
|
|
127
129
|
telemetry: { ...config.telemetry, ...(options.telemetry || {}) },
|
|
130
|
+
tools: {
|
|
131
|
+
includeCategories: config.tools?.includeCategories || [],
|
|
132
|
+
excludeCategories: config.tools?.excludeCategories || []
|
|
133
|
+
},
|
|
128
134
|
stdioEnabled: options.stdioEnabled !== undefined ? options.stdioEnabled : true
|
|
129
135
|
};
|
|
136
|
+
const toolCategoryPolicy = createToolCategoryPolicy(runtimeConfig.tools, logger);
|
|
137
|
+
let publicToolNames = null;
|
|
138
|
+
let toolFilterLogged = false;
|
|
139
|
+
|
|
140
|
+
const applyCategoryFilter = tools => {
|
|
141
|
+
if (!toolCategoryPolicy.isActive) {
|
|
142
|
+
return tools;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const filtered = filterToolsByCategory(tools, toolCategoryPolicy);
|
|
146
|
+
publicToolNames = filtered.publicToolNames;
|
|
147
|
+
|
|
148
|
+
if (!toolFilterLogged) {
|
|
149
|
+
const include =
|
|
150
|
+
toolCategoryPolicy.includeList.length > 0
|
|
151
|
+
? toolCategoryPolicy.includeList.join(', ')
|
|
152
|
+
: '(all)';
|
|
153
|
+
const exclude =
|
|
154
|
+
toolCategoryPolicy.excludeList.length > 0
|
|
155
|
+
? toolCategoryPolicy.excludeList.join(', ')
|
|
156
|
+
: '(none)';
|
|
157
|
+
logger.info(
|
|
158
|
+
`[MCP] Tool category filter enabled. include=${include}, exclude=${exclude}`
|
|
159
|
+
);
|
|
160
|
+
toolFilterLogged = true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return filtered.tools;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const projectInfoProvider =
|
|
167
|
+
deps.projectInfoProvider ||
|
|
168
|
+
(deps.ProjectInfoProvider ? new deps.ProjectInfoProvider() : null);
|
|
169
|
+
const projectRootGuard = createProjectRootGuard({
|
|
170
|
+
requireClientRoot: runtimeConfig.project?.requireClientRoot === true,
|
|
171
|
+
logger,
|
|
172
|
+
projectInfoProvider
|
|
173
|
+
});
|
|
130
174
|
|
|
131
175
|
// Step 2: Create a lightweight stdio MCP server (no TS SDK import)
|
|
132
176
|
const server =
|
|
@@ -180,7 +224,8 @@ export async function startServer(options = {}) {
|
|
|
180
224
|
port: runtimeConfig.http.port,
|
|
181
225
|
telemetryEnabled: runtimeConfig.telemetry.enabled,
|
|
182
226
|
healthPath: runtimeConfig.http.healthPath,
|
|
183
|
-
allowedHosts: runtimeConfig.http.allowedHosts
|
|
227
|
+
allowedHosts: runtimeConfig.http.allowedHosts,
|
|
228
|
+
requireClientRoot: runtimeConfig.project?.requireClientRoot === true
|
|
184
229
|
});
|
|
185
230
|
try {
|
|
186
231
|
await httpServerInstance.start();
|
|
@@ -320,14 +365,15 @@ export async function startServer(options = {}) {
|
|
|
320
365
|
server?.setRequestHandler('tools/list', async () => {
|
|
321
366
|
const manifestTools = readToolManifest();
|
|
322
367
|
if (manifestTools) {
|
|
323
|
-
|
|
368
|
+
const visibleTools = applyCategoryFilter(manifestTools);
|
|
369
|
+
logger.info(`[MCP] Returning ${visibleTools.length} tool definitions`);
|
|
324
370
|
requestPostInit();
|
|
325
|
-
return { tools:
|
|
371
|
+
return { tools: visibleTools };
|
|
326
372
|
}
|
|
327
373
|
|
|
328
374
|
await ensureInitialized(deps);
|
|
329
375
|
|
|
330
|
-
const
|
|
376
|
+
const allTools = Array.from(handlers.values())
|
|
331
377
|
.map((handler, index) => {
|
|
332
378
|
try {
|
|
333
379
|
const definition = handler.getDefinition();
|
|
@@ -345,6 +391,7 @@ export async function startServer(options = {}) {
|
|
|
345
391
|
})
|
|
346
392
|
.filter(tool => tool !== null);
|
|
347
393
|
|
|
394
|
+
const tools = applyCategoryFilter(allTools);
|
|
348
395
|
logger.info(`[MCP] Returning ${tools.length} tool definitions`);
|
|
349
396
|
requestPostInit();
|
|
350
397
|
return { tools };
|
|
@@ -362,6 +409,30 @@ export async function startServer(options = {}) {
|
|
|
362
409
|
{ args }
|
|
363
410
|
);
|
|
364
411
|
|
|
412
|
+
if (toolCategoryPolicy.isActive) {
|
|
413
|
+
if (!publicToolNames) {
|
|
414
|
+
const handlerTools = Array.from(handlers.values()).map(handler => ({ name: handler.name }));
|
|
415
|
+
publicToolNames = filterToolsByCategory(handlerTools, toolCategoryPolicy).publicToolNames;
|
|
416
|
+
}
|
|
417
|
+
if (!publicToolNames.has(name)) {
|
|
418
|
+
logger.error(`[MCP] Tool not found (filtered): ${name}`);
|
|
419
|
+
throw new Error(`Tool not found: ${name}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const guardError = await projectRootGuard(args || {});
|
|
424
|
+
if (guardError) {
|
|
425
|
+
logger.error(`[MCP] projectRoot guard failed: ${guardError}`);
|
|
426
|
+
return {
|
|
427
|
+
content: [
|
|
428
|
+
{
|
|
429
|
+
type: 'text',
|
|
430
|
+
text: `Error: ${guardError}\nCode: PROJECT_ROOT_MISMATCH`
|
|
431
|
+
}
|
|
432
|
+
]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
365
436
|
const handler = handlers.get(name);
|
|
366
437
|
if (!handler) {
|
|
367
438
|
logger.error(`[MCP] Tool not found: ${name}`);
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool category filtering for tools/list and tools/call visibility control.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const CATEGORY_NAMES = Object.freeze([
|
|
6
|
+
'system',
|
|
7
|
+
'gameobject',
|
|
8
|
+
'scene',
|
|
9
|
+
'analysis',
|
|
10
|
+
'playmode',
|
|
11
|
+
'ui',
|
|
12
|
+
'input',
|
|
13
|
+
'asset',
|
|
14
|
+
'prefab',
|
|
15
|
+
'material',
|
|
16
|
+
'addressables',
|
|
17
|
+
'menu',
|
|
18
|
+
'console',
|
|
19
|
+
'screenshot',
|
|
20
|
+
'video',
|
|
21
|
+
'component',
|
|
22
|
+
'compilation',
|
|
23
|
+
'test',
|
|
24
|
+
'editor',
|
|
25
|
+
'settings',
|
|
26
|
+
'package',
|
|
27
|
+
'script',
|
|
28
|
+
'profiler',
|
|
29
|
+
'general'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function toCategoryKey(value) {
|
|
33
|
+
if (typeof value !== 'string') return '';
|
|
34
|
+
return value
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[\s_-]+/g, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CATEGORY_KEY_TO_NAME = new Map(CATEGORY_NAMES.map(name => [toCategoryKey(name), name]));
|
|
41
|
+
|
|
42
|
+
// User-friendly aliases
|
|
43
|
+
CATEGORY_KEY_TO_NAME.set('addressable', 'addressables');
|
|
44
|
+
CATEGORY_KEY_TO_NAME.set('gameobj', 'gameobject');
|
|
45
|
+
CATEGORY_KEY_TO_NAME.set('play', 'playmode');
|
|
46
|
+
CATEGORY_KEY_TO_NAME.set('playmode', 'playmode');
|
|
47
|
+
CATEGORY_KEY_TO_NAME.set('uitoolkit', 'ui');
|
|
48
|
+
CATEGORY_KEY_TO_NAME.set('ugui', 'ui');
|
|
49
|
+
CATEGORY_KEY_TO_NAME.set('imgui', 'ui');
|
|
50
|
+
|
|
51
|
+
const SYSTEM_TOOLS = new Set([
|
|
52
|
+
'ping',
|
|
53
|
+
'refresh_assets',
|
|
54
|
+
'get_command_stats',
|
|
55
|
+
'get_server_info',
|
|
56
|
+
'search_tools'
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const GAMEOBJECT_TOOLS = new Set([
|
|
60
|
+
'create_gameobject',
|
|
61
|
+
'find_gameobject',
|
|
62
|
+
'modify_gameobject',
|
|
63
|
+
'delete_gameobject',
|
|
64
|
+
'get_hierarchy'
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const SCENE_TOOLS = new Set(['create_scene', 'load_scene', 'save_scene', 'list_scenes', 'get_scene_info']);
|
|
68
|
+
|
|
69
|
+
const ANALYSIS_TOOLS = new Set([
|
|
70
|
+
'get_gameobject_details',
|
|
71
|
+
'analyze_scene_contents',
|
|
72
|
+
'get_component_values',
|
|
73
|
+
'find_by_component',
|
|
74
|
+
'get_object_references',
|
|
75
|
+
'get_animator_state',
|
|
76
|
+
'get_animator_runtime_info',
|
|
77
|
+
'get_input_actions_state',
|
|
78
|
+
'analyze_input_actions_asset'
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const PLAYMODE_TOOLS = new Set(['play_game', 'pause_game', 'stop_game', 'playmode_wait_for_state']);
|
|
82
|
+
|
|
83
|
+
const UI_TOOLS = new Set([
|
|
84
|
+
'find_ui_elements',
|
|
85
|
+
'click_ui_element',
|
|
86
|
+
'get_ui_element_state',
|
|
87
|
+
'set_ui_element_value',
|
|
88
|
+
'simulate_ui_input'
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const INPUT_TOOLS = new Set([
|
|
92
|
+
'input_system_control',
|
|
93
|
+
'input_keyboard',
|
|
94
|
+
'input_mouse',
|
|
95
|
+
'input_gamepad',
|
|
96
|
+
'input_touch',
|
|
97
|
+
'create_action_map',
|
|
98
|
+
'remove_action_map',
|
|
99
|
+
'add_input_action',
|
|
100
|
+
'remove_input_action',
|
|
101
|
+
'add_input_binding',
|
|
102
|
+
'remove_input_binding',
|
|
103
|
+
'remove_all_bindings',
|
|
104
|
+
'create_composite_binding',
|
|
105
|
+
'manage_control_schemes'
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const ASSET_TOOLS = new Set([
|
|
109
|
+
'manage_asset_database',
|
|
110
|
+
'manage_asset_import_settings',
|
|
111
|
+
'analyze_asset_dependencies'
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const PREFAB_TOOLS = new Set([
|
|
115
|
+
'create_prefab',
|
|
116
|
+
'modify_prefab',
|
|
117
|
+
'instantiate_prefab',
|
|
118
|
+
'open_prefab',
|
|
119
|
+
'exit_prefab_mode',
|
|
120
|
+
'save_prefab'
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const MATERIAL_TOOLS = new Set(['create_material', 'modify_material']);
|
|
124
|
+
|
|
125
|
+
const MENU_TOOLS = new Set(['execute_menu_item']);
|
|
126
|
+
const CONSOLE_TOOLS = new Set(['clear_console', 'read_console']);
|
|
127
|
+
const SCREENSHOT_TOOLS = new Set(['capture_screenshot', 'analyze_screenshot']);
|
|
128
|
+
const VIDEO_TOOLS = new Set([
|
|
129
|
+
'capture_video_start',
|
|
130
|
+
'capture_video_stop',
|
|
131
|
+
'capture_video_status',
|
|
132
|
+
'video_capture_for'
|
|
133
|
+
]);
|
|
134
|
+
const COMPONENT_TOOLS = new Set([
|
|
135
|
+
'add_component',
|
|
136
|
+
'remove_component',
|
|
137
|
+
'modify_component',
|
|
138
|
+
'list_components',
|
|
139
|
+
'get_component_types',
|
|
140
|
+
'set_component_field'
|
|
141
|
+
]);
|
|
142
|
+
const COMPILATION_TOOLS = new Set(['get_compilation_state']);
|
|
143
|
+
const TEST_TOOLS = new Set(['run_tests', 'get_test_status']);
|
|
144
|
+
const EDITOR_TOOLS = new Set([
|
|
145
|
+
'manage_tags',
|
|
146
|
+
'manage_layers',
|
|
147
|
+
'manage_selection',
|
|
148
|
+
'manage_windows',
|
|
149
|
+
'manage_tools',
|
|
150
|
+
'get_editor_state',
|
|
151
|
+
'quit_editor'
|
|
152
|
+
]);
|
|
153
|
+
const SETTINGS_TOOLS = new Set(['get_project_settings', 'update_project_settings']);
|
|
154
|
+
const PACKAGE_TOOLS = new Set(['package_manager', 'registry_config', 'list_packages']);
|
|
155
|
+
const SCRIPT_TOOLS = new Set([
|
|
156
|
+
'read',
|
|
157
|
+
'search',
|
|
158
|
+
'edit_structured',
|
|
159
|
+
'edit_snippet',
|
|
160
|
+
'get_symbols',
|
|
161
|
+
'find_symbol',
|
|
162
|
+
'find_refs',
|
|
163
|
+
'build_index',
|
|
164
|
+
'update_index',
|
|
165
|
+
'get_index_status',
|
|
166
|
+
'rename_symbol',
|
|
167
|
+
'create_class',
|
|
168
|
+
'remove_symbol'
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
const TOOL_SETS = [
|
|
172
|
+
['system', SYSTEM_TOOLS],
|
|
173
|
+
['gameobject', GAMEOBJECT_TOOLS],
|
|
174
|
+
['scene', SCENE_TOOLS],
|
|
175
|
+
['analysis', ANALYSIS_TOOLS],
|
|
176
|
+
['playmode', PLAYMODE_TOOLS],
|
|
177
|
+
['ui', UI_TOOLS],
|
|
178
|
+
['input', INPUT_TOOLS],
|
|
179
|
+
['asset', ASSET_TOOLS],
|
|
180
|
+
['prefab', PREFAB_TOOLS],
|
|
181
|
+
['material', MATERIAL_TOOLS],
|
|
182
|
+
['menu', MENU_TOOLS],
|
|
183
|
+
['console', CONSOLE_TOOLS],
|
|
184
|
+
['screenshot', SCREENSHOT_TOOLS],
|
|
185
|
+
['video', VIDEO_TOOLS],
|
|
186
|
+
['component', COMPONENT_TOOLS],
|
|
187
|
+
['compilation', COMPILATION_TOOLS],
|
|
188
|
+
['test', TEST_TOOLS],
|
|
189
|
+
['editor', EDITOR_TOOLS],
|
|
190
|
+
['settings', SETTINGS_TOOLS],
|
|
191
|
+
['package', PACKAGE_TOOLS],
|
|
192
|
+
['script', SCRIPT_TOOLS]
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
function logWarning(logger, message) {
|
|
196
|
+
if (!logger) return;
|
|
197
|
+
if (typeof logger.warning === 'function') {
|
|
198
|
+
logger.warning(message);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (typeof logger.warn === 'function') {
|
|
202
|
+
logger.warn(message);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (typeof logger.info === 'function') {
|
|
206
|
+
logger.info(message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeCategoryList(rawValues = []) {
|
|
211
|
+
const values = Array.isArray(rawValues) ? rawValues : [];
|
|
212
|
+
const normalized = [];
|
|
213
|
+
const unknown = [];
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
|
|
216
|
+
for (const value of values) {
|
|
217
|
+
const key = toCategoryKey(value);
|
|
218
|
+
if (!key) continue;
|
|
219
|
+
const category = CATEGORY_KEY_TO_NAME.get(key);
|
|
220
|
+
if (!category) {
|
|
221
|
+
unknown.push(String(value).trim());
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (seen.has(category)) continue;
|
|
225
|
+
seen.add(category);
|
|
226
|
+
normalized.push(category);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { normalized, unknown };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getToolCategory(toolName) {
|
|
233
|
+
const name = typeof toolName === 'string' ? toolName : '';
|
|
234
|
+
if (!name) return 'general';
|
|
235
|
+
if (name.startsWith('addressables_')) return 'addressables';
|
|
236
|
+
if (name.startsWith('profiler_')) return 'profiler';
|
|
237
|
+
|
|
238
|
+
for (const [category, set] of TOOL_SETS) {
|
|
239
|
+
if (set.has(name)) return category;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return 'general';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function createToolCategoryPolicy(config = {}, logger) {
|
|
246
|
+
const includeResult = normalizeCategoryList(config.includeCategories);
|
|
247
|
+
const excludeResult = normalizeCategoryList(config.excludeCategories);
|
|
248
|
+
|
|
249
|
+
if (includeResult.unknown.length > 0) {
|
|
250
|
+
logWarning(
|
|
251
|
+
logger,
|
|
252
|
+
`[tool-filter] Ignoring unknown include categories: ${includeResult.unknown.join(', ')}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (excludeResult.unknown.length > 0) {
|
|
256
|
+
logWarning(
|
|
257
|
+
logger,
|
|
258
|
+
`[tool-filter] Ignoring unknown exclude categories: ${excludeResult.unknown.join(', ')}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
includeCategories: new Set(includeResult.normalized),
|
|
264
|
+
excludeCategories: new Set(excludeResult.normalized),
|
|
265
|
+
includeList: includeResult.normalized,
|
|
266
|
+
excludeList: excludeResult.normalized,
|
|
267
|
+
isActive: includeResult.normalized.length > 0 || excludeResult.normalized.length > 0
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function filterToolsByCategory(tools, policy) {
|
|
272
|
+
const list = Array.isArray(tools) ? tools : [];
|
|
273
|
+
const publicTools = [];
|
|
274
|
+
const hiddenToolNames = new Set();
|
|
275
|
+
const publicToolNames = new Set();
|
|
276
|
+
const includeCategories = policy?.includeCategories ?? new Set();
|
|
277
|
+
const excludeCategories = policy?.excludeCategories ?? new Set();
|
|
278
|
+
const includeEnabled = includeCategories.size > 0;
|
|
279
|
+
const excludeEnabled = excludeCategories.size > 0;
|
|
280
|
+
|
|
281
|
+
for (const tool of list) {
|
|
282
|
+
const name = tool?.name;
|
|
283
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const category = getToolCategory(name);
|
|
288
|
+
const includeMatch = !includeEnabled || includeCategories.has(category);
|
|
289
|
+
const excludeMatch = excludeEnabled && excludeCategories.has(category);
|
|
290
|
+
const visible = includeMatch && !excludeMatch;
|
|
291
|
+
|
|
292
|
+
if (visible) {
|
|
293
|
+
publicTools.push(tool);
|
|
294
|
+
publicToolNames.add(name);
|
|
295
|
+
} else {
|
|
296
|
+
hiddenToolNames.add(name);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
tools: publicTools,
|
|
302
|
+
publicToolNames,
|
|
303
|
+
hiddenToolNames
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function getKnownToolCategories() {
|
|
308
|
+
return [...CATEGORY_NAMES];
|
|
309
|
+
}
|
|
@@ -3602,6 +3602,14 @@
|
|
|
3602
3602
|
"properties": {}
|
|
3603
3603
|
}
|
|
3604
3604
|
},
|
|
3605
|
+
{
|
|
3606
|
+
"name": "get_server_info",
|
|
3607
|
+
"description": "Get MCP server identifying information (pid, project root, workspace)",
|
|
3608
|
+
"inputSchema": {
|
|
3609
|
+
"type": "object",
|
|
3610
|
+
"properties": {}
|
|
3611
|
+
}
|
|
3612
|
+
},
|
|
3605
3613
|
{
|
|
3606
3614
|
"name": "ping",
|
|
3607
3615
|
"description": "Test connection to Unity Editor",
|
|
@@ -20,6 +20,7 @@ import fs from 'fs';
|
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import os from 'os';
|
|
22
22
|
import { fileURLToPath } from 'url';
|
|
23
|
+
import { buildProgress, getReportEvery } from '../indexProgress.js';
|
|
23
24
|
|
|
24
25
|
// fast-sql helper: run SQL statement
|
|
25
26
|
function runSQL(db, sql) {
|
|
@@ -71,8 +72,8 @@ function log(level, message) {
|
|
|
71
72
|
sendMessage('log', { level, message });
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
function sendProgress(processed, total, rate) {
|
|
75
|
-
sendMessage('progress', { data: { processed, total, rate } });
|
|
75
|
+
function sendProgress(phase, processed, total, rate) {
|
|
76
|
+
sendMessage('progress', { data: buildProgress({ phase, processed, total, rate }) });
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
function sendComplete(result) {
|
|
@@ -624,6 +625,8 @@ async function runBuild() {
|
|
|
624
625
|
|
|
625
626
|
// Determine changes (this calls makeSig for each file)
|
|
626
627
|
log('info', `[worker] Computing file signatures (${files.length} files)...`);
|
|
628
|
+
sendProgress('signature', 0, files.length, 0);
|
|
629
|
+
const signatureReportEvery = getReportEvery(files.length);
|
|
627
630
|
const sigStartTime = Date.now();
|
|
628
631
|
let sigProcessed = 0;
|
|
629
632
|
for (const abs of files) {
|
|
@@ -632,13 +635,19 @@ async function runBuild() {
|
|
|
632
635
|
wanted.set(rel, sig);
|
|
633
636
|
sigProcessed++;
|
|
634
637
|
// Report progress every 10000 files
|
|
635
|
-
if (sigProcessed %
|
|
638
|
+
if (sigProcessed % signatureReportEvery === 0) {
|
|
636
639
|
const elapsed = ((Date.now() - sigStartTime) / 1000).toFixed(1);
|
|
637
640
|
log('info', `[worker] Signature progress: ${sigProcessed}/${files.length} (${elapsed}s)`);
|
|
641
|
+
const sigElapsed = Math.max(1, Date.now() - sigStartTime);
|
|
642
|
+
const sigRate = parseFloat(((sigProcessed * 1000) / sigElapsed).toFixed(1));
|
|
643
|
+
sendProgress('signature', sigProcessed, files.length, sigRate);
|
|
638
644
|
}
|
|
639
645
|
}
|
|
640
646
|
const sigTime = ((Date.now() - sigStartTime) / 1000).toFixed(1);
|
|
641
647
|
log('info', `[worker] Signatures computed in ${sigTime}s`);
|
|
648
|
+
const sigElapsed = Math.max(1, Date.now() - sigStartTime);
|
|
649
|
+
const sigRate = parseFloat(((sigProcessed * 1000) / sigElapsed).toFixed(1));
|
|
650
|
+
sendProgress('signature', sigProcessed, files.length, sigRate);
|
|
642
651
|
|
|
643
652
|
for (const [rel, sig] of wanted) {
|
|
644
653
|
if (current.get(rel) !== sig) changed.push(rel);
|
|
@@ -671,6 +680,7 @@ async function runBuild() {
|
|
|
671
680
|
|
|
672
681
|
// Prepare for updates
|
|
673
682
|
const absList = changed.map(rel => path.resolve(projectRoot, rel));
|
|
683
|
+
sendProgress('index', 0, absList.length, 0);
|
|
674
684
|
const startAt = Date.now();
|
|
675
685
|
let processed = 0;
|
|
676
686
|
let updated = 0;
|
|
@@ -786,7 +796,7 @@ async function runBuild() {
|
|
|
786
796
|
currentPercentage >= lastReportedPercentage + reportPercentage ||
|
|
787
797
|
processed === absList.length
|
|
788
798
|
) {
|
|
789
|
-
sendProgress(processed, absList.length, rate);
|
|
799
|
+
sendProgress('index', processed, absList.length, rate);
|
|
790
800
|
log(
|
|
791
801
|
'info',
|
|
792
802
|
`[worker] progress ${currentPercentage}% (${processed}/${absList.length}) rate:${rate} f/s`
|
package/src/handlers/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { SystemPingToolHandler } from './system/SystemPingToolHandler.js';
|
|
11
11
|
import { SystemRefreshAssetsToolHandler } from './system/SystemRefreshAssetsToolHandler.js';
|
|
12
12
|
import { SystemGetCommandStatsToolHandler } from './system/SystemGetCommandStatsToolHandler.js';
|
|
13
|
+
import { SystemGetServerInfoToolHandler } from './system/SystemGetServerInfoToolHandler.js';
|
|
13
14
|
import { GameObjectCreateToolHandler } from './gameobject/GameObjectCreateToolHandler.js';
|
|
14
15
|
import { GameObjectFindToolHandler } from './gameobject/GameObjectFindToolHandler.js';
|
|
15
16
|
import { GameObjectModifyToolHandler } from './gameobject/GameObjectModifyToolHandler.js';
|
|
@@ -279,6 +280,7 @@ const HANDLER_CLASSES = [
|
|
|
279
280
|
SystemPingToolHandler,
|
|
280
281
|
SystemRefreshAssetsToolHandler,
|
|
281
282
|
SystemGetCommandStatsToolHandler,
|
|
283
|
+
SystemGetServerInfoToolHandler,
|
|
282
284
|
|
|
283
285
|
// GameObject handlers
|
|
284
286
|
GameObjectCreateToolHandler,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
2
|
import { JobManager } from '../../core/jobManager.js';
|
|
3
3
|
import { CodeIndex } from '../../core/codeIndex.js';
|
|
4
|
+
import { getWorkerPool } from '../../core/indexBuildWorkerPool.js';
|
|
4
5
|
|
|
5
6
|
export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
6
7
|
constructor(unityConnection) {
|
|
@@ -16,6 +17,7 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
16
17
|
this.unityConnection = unityConnection;
|
|
17
18
|
this.jobManager = JobManager.getInstance();
|
|
18
19
|
this.codeIndex = new CodeIndex(unityConnection);
|
|
20
|
+
this.workerPool = getWorkerPool();
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
async execute() {
|
|
@@ -53,7 +55,8 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
53
55
|
}
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
|
-
const
|
|
58
|
+
const workerBuildRunning = Boolean(this.workerPool?.isRunning?.());
|
|
59
|
+
const buildInProgress = latestBuildJob?.status === 'running' || workerBuildRunning;
|
|
57
60
|
if (!ready && !buildInProgress) {
|
|
58
61
|
if (latestBuildJob) {
|
|
59
62
|
const indexInfo = {
|
|
@@ -93,6 +96,51 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
93
96
|
message: 'Code index is not built. Please run UnityMCP.build_index first.'
|
|
94
97
|
};
|
|
95
98
|
}
|
|
99
|
+
if (!ready && buildInProgress) {
|
|
100
|
+
const buildJob = latestBuildJob
|
|
101
|
+
? {
|
|
102
|
+
id: latestBuildJob.id,
|
|
103
|
+
status: latestBuildJob.status,
|
|
104
|
+
startedAt: latestBuildJob.startedAt ?? null,
|
|
105
|
+
...(latestBuildJob.progress
|
|
106
|
+
? {
|
|
107
|
+
progress: {
|
|
108
|
+
phase: latestBuildJob.progress.phase || 'index',
|
|
109
|
+
...latestBuildJob.progress
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
: {})
|
|
113
|
+
}
|
|
114
|
+
: {
|
|
115
|
+
id: null,
|
|
116
|
+
status: 'running',
|
|
117
|
+
startedAt: null,
|
|
118
|
+
source: 'worker'
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (latestBuildJob?.status === 'failed') {
|
|
122
|
+
buildJob.failedAt = latestBuildJob.failedAt ?? null;
|
|
123
|
+
buildJob.error = latestBuildJob.error ?? 'Unknown error';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
success: true,
|
|
128
|
+
status: latestBuildJob?.status === 'failed' ? 'failed' : 'pending',
|
|
129
|
+
ready: false,
|
|
130
|
+
totalFiles: 0,
|
|
131
|
+
indexedFiles: 0,
|
|
132
|
+
coverage: 0,
|
|
133
|
+
message: buildInProgress
|
|
134
|
+
? 'Code index build is running. Check back with get_index_status.'
|
|
135
|
+
: 'Code index is not ready yet.',
|
|
136
|
+
index: {
|
|
137
|
+
ready: false,
|
|
138
|
+
rows: 0,
|
|
139
|
+
lastIndexedAt: null,
|
|
140
|
+
buildJob
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
96
144
|
|
|
97
145
|
// Use DB stats directly for fast status check - avoid expensive filesystem traversal
|
|
98
146
|
const stats = await this.codeIndex.getStats();
|
|
@@ -109,7 +157,9 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
109
157
|
};
|
|
110
158
|
|
|
111
159
|
if (latestBuildJob) {
|
|
112
|
-
const progress = latestBuildJob.progress
|
|
160
|
+
const progress = latestBuildJob.progress
|
|
161
|
+
? { phase: latestBuildJob.progress.phase || 'index', ...latestBuildJob.progress }
|
|
162
|
+
: undefined;
|
|
113
163
|
indexInfo.buildJob = {
|
|
114
164
|
id: latestBuildJob.id,
|
|
115
165
|
status: latestBuildJob.status,
|
|
@@ -5,6 +5,7 @@ import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
|
5
5
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
6
6
|
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
7
7
|
import { preSyntaxCheck } from './csharpSyntaxCheck.js';
|
|
8
|
+
import { logger } from '../../core/config.js';
|
|
8
9
|
|
|
9
10
|
const MAX_INSTRUCTIONS = 10;
|
|
10
11
|
const MAX_DIFF_CHARS = 80;
|
|
@@ -29,11 +30,6 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
29
30
|
description:
|
|
30
31
|
'If true, run validation and return preview text without writing to disk. Default=false.'
|
|
31
32
|
},
|
|
32
|
-
skipValidation: {
|
|
33
|
-
type: 'boolean',
|
|
34
|
-
description:
|
|
35
|
-
'If true, skip LSP validation for faster execution. Lightweight syntax checks (brace balance) are still performed. Use for simple edits on large files. Default=false.'
|
|
36
|
-
},
|
|
37
33
|
instructions: {
|
|
38
34
|
type: 'array',
|
|
39
35
|
minItems: 1,
|
|
@@ -84,7 +80,10 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
84
80
|
|
|
85
81
|
validate(params) {
|
|
86
82
|
super.validate(params);
|
|
87
|
-
const { path: filePath, instructions } = params;
|
|
83
|
+
const { path: filePath, instructions, skipValidation } = params;
|
|
84
|
+
if (skipValidation === true) {
|
|
85
|
+
throw new Error('skipValidation is not allowed; LSP validation is required');
|
|
86
|
+
}
|
|
88
87
|
if (!filePath || String(filePath).trim() === '') {
|
|
89
88
|
throw new Error('path cannot be empty');
|
|
90
89
|
}
|
|
@@ -115,10 +114,17 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
async execute(params) {
|
|
117
|
+
logger.info(
|
|
118
|
+
`[Handler edit_snippet] pid=${process.pid} path=${params?.path || ''} preview=${
|
|
119
|
+
params?.preview === true
|
|
120
|
+
}`
|
|
121
|
+
);
|
|
122
|
+
if (params?.skipValidation === true) {
|
|
123
|
+
throw new Error('skipValidation is not allowed; LSP validation is required');
|
|
124
|
+
}
|
|
118
125
|
const info = await this.projectInfo.get();
|
|
119
126
|
const { relative, absolute } = this.#resolvePaths(info, params.path);
|
|
120
127
|
const preview = params.preview === true;
|
|
121
|
-
const skipValidation = params.skipValidation === true;
|
|
122
128
|
const instructions = params.instructions;
|
|
123
129
|
|
|
124
130
|
let original;
|
|
@@ -152,7 +158,7 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
152
158
|
results,
|
|
153
159
|
original,
|
|
154
160
|
updated: working,
|
|
155
|
-
validationSkipped:
|
|
161
|
+
validationSkipped: false
|
|
156
162
|
});
|
|
157
163
|
}
|
|
158
164
|
|
|
@@ -165,16 +171,14 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
165
171
|
);
|
|
166
172
|
}
|
|
167
173
|
|
|
168
|
-
// LSP validation
|
|
174
|
+
// LSP validation
|
|
169
175
|
let diagnostics = [];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
throw new Error(`syntax_error: ${msg}`);
|
|
177
|
-
}
|
|
176
|
+
diagnostics = await this.#validateWithLsp(info, relative, working);
|
|
177
|
+
const hasErrors = diagnostics.some(d => this.#severityIsError(d.severity));
|
|
178
|
+
if (hasErrors) {
|
|
179
|
+
const first = diagnostics.find(d => this.#severityIsError(d.severity));
|
|
180
|
+
const msg = first?.message || 'syntax error';
|
|
181
|
+
throw new Error(`syntax_error: ${msg}`);
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
if (!preview) {
|
|
@@ -187,7 +191,7 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
187
191
|
original,
|
|
188
192
|
updated: working,
|
|
189
193
|
diagnostics,
|
|
190
|
-
validationSkipped:
|
|
194
|
+
validationSkipped: false
|
|
191
195
|
});
|
|
192
196
|
}
|
|
193
197
|
|
|
@@ -298,7 +302,30 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
298
302
|
if (!this.lsp) {
|
|
299
303
|
this.lsp = await LspRpcClientSingleton.getValidationInstance(info.projectRoot);
|
|
300
304
|
}
|
|
301
|
-
|
|
305
|
+
const tempRelative = this.#buildTempValidationPath(relative);
|
|
306
|
+
const tempAbsolute = path.join(
|
|
307
|
+
info.projectRoot,
|
|
308
|
+
tempRelative.replace(/\//g, path.sep)
|
|
309
|
+
);
|
|
310
|
+
await fs.mkdir(path.dirname(tempAbsolute), { recursive: true });
|
|
311
|
+
await fs.writeFile(tempAbsolute, updatedText, 'utf8');
|
|
312
|
+
try {
|
|
313
|
+
return await this.lsp.validateText(tempRelative, '');
|
|
314
|
+
} finally {
|
|
315
|
+
try {
|
|
316
|
+
await fs.rm(tempAbsolute, { force: true });
|
|
317
|
+
} catch (e) {
|
|
318
|
+
logger.warning(`[Handler edit_snippet] failed to remove temp file: ${e.message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#buildTempValidationPath(relative) {
|
|
324
|
+
const ext = path.extname(relative) || '.cs';
|
|
325
|
+
const base = path.basename(relative, ext).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
326
|
+
const stamp = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
327
|
+
const hash = crypto.createHash('sha1').update(relative).digest('hex').slice(0, 8);
|
|
328
|
+
return `.unity/tmp/edit-snippet/${base}_${hash}_${stamp}${ext}`;
|
|
302
329
|
}
|
|
303
330
|
|
|
304
331
|
#buildResponse({
|
|
@@ -351,4 +378,5 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
351
378
|
const s = String(severity).toLowerCase();
|
|
352
379
|
return s === 'error' || s === '2';
|
|
353
380
|
}
|
|
381
|
+
|
|
354
382
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { CATEGORIES, SCOPES } from '../base/categories.js';
|
|
3
|
+
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
4
|
+
import { config, WORKSPACE_ROOT } from '../../core/config.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handler for the get_server_info tool
|
|
8
|
+
* Provides identifiers to distinguish multiple MCP servers
|
|
9
|
+
*/
|
|
10
|
+
export class SystemGetServerInfoToolHandler extends BaseToolHandler {
|
|
11
|
+
constructor(unityConnection) {
|
|
12
|
+
super(
|
|
13
|
+
'get_server_info',
|
|
14
|
+
'Get MCP server identifying information (pid, project root, workspace)',
|
|
15
|
+
{
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
category: CATEGORIES.SYSTEM,
|
|
21
|
+
scope: SCOPES.READ,
|
|
22
|
+
keywords: ['server', 'info', 'pid', 'project', 'workspace', 'identify'],
|
|
23
|
+
tags: ['system', 'diagnostic']
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
this.unityConnection = unityConnection;
|
|
28
|
+
this.projectInfo = new ProjectInfoProvider();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async execute() {
|
|
32
|
+
const info = await this.projectInfo.get();
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
pid: process.pid,
|
|
36
|
+
projectRoot: info.projectRoot,
|
|
37
|
+
assetsPath: info.assetsPath,
|
|
38
|
+
packagesPath: info.packagesPath,
|
|
39
|
+
codeIndexRoot: info.codeIndexRoot,
|
|
40
|
+
workspaceRoot: WORKSPACE_ROOT,
|
|
41
|
+
server: {
|
|
42
|
+
name: config?.server?.name,
|
|
43
|
+
version: config?.server?.version
|
|
44
|
+
},
|
|
45
|
+
unity: {
|
|
46
|
+
host: config?.unity?.unityHost ?? config?.unity?.mcpHost,
|
|
47
|
+
port: config?.unity?.port
|
|
48
|
+
},
|
|
49
|
+
http: {
|
|
50
|
+
enabled: config?.http?.enabled,
|
|
51
|
+
port: config?.http?.port
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/lsp/LspRpcClient.js
CHANGED
|
@@ -152,12 +152,19 @@ export class LspRpcClient {
|
|
|
152
152
|
return resp;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
async request(method, params) {
|
|
156
|
-
return await this.#requestWithRetry(method, params, 1);
|
|
155
|
+
async request(method, params, options = {}) {
|
|
156
|
+
return await this.#requestWithRetry(method, params, 1, options);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
async validateText(relative, newText) {
|
|
160
|
-
const
|
|
160
|
+
const timeoutMs = Number.isFinite(config.lsp?.validationTimeoutMs)
|
|
161
|
+
? config.lsp.validationTimeoutMs
|
|
162
|
+
: undefined;
|
|
163
|
+
const resp = await this.request(
|
|
164
|
+
'mcp/validateTextEdits',
|
|
165
|
+
{ relative, newText },
|
|
166
|
+
{ timeoutMs }
|
|
167
|
+
);
|
|
161
168
|
if (!resp) return [];
|
|
162
169
|
const payload = resp.result ?? resp;
|
|
163
170
|
const diagnostics = Array.isArray(payload?.diagnostics) ? payload.diagnostics : [];
|
|
@@ -170,10 +177,14 @@ export class LspRpcClient {
|
|
|
170
177
|
}));
|
|
171
178
|
}
|
|
172
179
|
|
|
173
|
-
async #requestWithRetry(method, params, attempt) {
|
|
180
|
+
async #requestWithRetry(method, params, attempt, options) {
|
|
174
181
|
let id = null;
|
|
175
182
|
let timeoutHandle = null;
|
|
176
|
-
const
|
|
183
|
+
const configuredTimeout = config.lsp?.requestTimeoutMs || 60000;
|
|
184
|
+
const overrideTimeout = options?.timeoutMs;
|
|
185
|
+
const timeoutMs = Number.isFinite(overrideTimeout)
|
|
186
|
+
? Math.max(1000, Math.min(300000, overrideTimeout))
|
|
187
|
+
: Math.max(1000, Math.min(300000, configuredTimeout));
|
|
177
188
|
const startedAt = Date.now();
|
|
178
189
|
try {
|
|
179
190
|
await this.ensure();
|
|
@@ -227,7 +238,7 @@ export class LspRpcClient {
|
|
|
227
238
|
logger.warning(
|
|
228
239
|
`[unity-mcp-server:lsp] recoverable error on ${method}: ${msg}. Retrying once...`
|
|
229
240
|
);
|
|
230
|
-
return await this.#requestWithRetry(method, params, attempt + 1);
|
|
241
|
+
return await this.#requestWithRetry(method, params, attempt + 1, options);
|
|
231
242
|
}
|
|
232
243
|
// Standardize error message with actionable recovery instructions
|
|
233
244
|
let hint;
|