@akiojin/unity-mcp-server 5.3.2 → 5.4.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 +1 -1
- package/src/core/config.js +21 -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 +25 -1
- 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
package/src/core/config.js
CHANGED
|
@@ -105,7 +105,8 @@ const baseConfig = {
|
|
|
105
105
|
// Project settings (primarily for code index paths)
|
|
106
106
|
project: {
|
|
107
107
|
root: null,
|
|
108
|
-
codeIndexRoot: null
|
|
108
|
+
codeIndexRoot: null,
|
|
109
|
+
requireClientRoot: false
|
|
109
110
|
},
|
|
110
111
|
|
|
111
112
|
// Server settings
|
|
@@ -154,7 +155,8 @@ const baseConfig = {
|
|
|
154
155
|
// LSP client defaults
|
|
155
156
|
lsp: {
|
|
156
157
|
requestTimeoutMs: 120000,
|
|
157
|
-
slowRequestWarnMs: 2000
|
|
158
|
+
slowRequestWarnMs: 2000,
|
|
159
|
+
validationTimeoutMs: 5000
|
|
158
160
|
},
|
|
159
161
|
|
|
160
162
|
// Indexing (code index) settings
|
|
@@ -176,6 +178,7 @@ function loadEnvConfig() {
|
|
|
176
178
|
const unityPort = parseIntEnv(process.env.UNITY_MCP_PORT);
|
|
177
179
|
|
|
178
180
|
const projectRoot = envString('UNITY_PROJECT_ROOT');
|
|
181
|
+
const requireProjectRoot = parseBoolEnv(process.env.UNITY_MCP_REQUIRE_PROJECT_ROOT);
|
|
179
182
|
|
|
180
183
|
const logLevel = envString('UNITY_MCP_LOG_LEVEL');
|
|
181
184
|
const versionMismatch = envString('UNITY_MCP_VERSION_MISMATCH');
|
|
@@ -186,6 +189,7 @@ function loadEnvConfig() {
|
|
|
186
189
|
const telemetryEnabled = parseBoolEnv(process.env.UNITY_MCP_TELEMETRY_ENABLED);
|
|
187
190
|
const lspRequestTimeoutMs = parseIntEnv(process.env.UNITY_MCP_LSP_REQUEST_TIMEOUT_MS);
|
|
188
191
|
const lspSlowRequestWarnMs = parseIntEnv(process.env.UNITY_MCP_LSP_SLOW_REQUEST_WARN_MS);
|
|
192
|
+
const lspValidationTimeoutMs = parseIntEnv(process.env.UNITY_MCP_LSP_VALIDATION_TIMEOUT_MS);
|
|
189
193
|
|
|
190
194
|
const out = {};
|
|
191
195
|
|
|
@@ -201,6 +205,9 @@ function loadEnvConfig() {
|
|
|
201
205
|
out.project = {};
|
|
202
206
|
if (projectRoot) out.project.root = projectRoot;
|
|
203
207
|
}
|
|
208
|
+
if (requireProjectRoot !== undefined) {
|
|
209
|
+
out.project = { ...(out.project || {}), requireClientRoot: requireProjectRoot };
|
|
210
|
+
}
|
|
204
211
|
|
|
205
212
|
if (logLevel) {
|
|
206
213
|
out.logging = { level: logLevel };
|
|
@@ -226,6 +233,9 @@ function loadEnvConfig() {
|
|
|
226
233
|
if (lspSlowRequestWarnMs !== undefined) {
|
|
227
234
|
out.lsp = { ...(out.lsp || {}), slowRequestWarnMs: lspSlowRequestWarnMs };
|
|
228
235
|
}
|
|
236
|
+
if (lspValidationTimeoutMs !== undefined) {
|
|
237
|
+
out.lsp = { ...(out.lsp || {}), validationTimeoutMs: lspValidationTimeoutMs };
|
|
238
|
+
}
|
|
229
239
|
|
|
230
240
|
return out;
|
|
231
241
|
}
|
|
@@ -315,6 +325,15 @@ function validateAndNormalizeConfig(cfg) {
|
|
|
315
325
|
cfg.lsp.slowRequestWarnMs = 2000;
|
|
316
326
|
}
|
|
317
327
|
}
|
|
328
|
+
if (cfg.lsp?.validationTimeoutMs !== undefined) {
|
|
329
|
+
const t = Number(cfg.lsp.validationTimeoutMs);
|
|
330
|
+
if (!Number.isFinite(t) || t < 0) {
|
|
331
|
+
logger.warning(
|
|
332
|
+
`[unity-mcp-server] WARN: Invalid UNITY_MCP_LSP_VALIDATION_TIMEOUT_MS (${cfg.lsp.validationTimeoutMs}); using default 5000`
|
|
333
|
+
);
|
|
334
|
+
cfg.lsp.validationTimeoutMs = 5000;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
318
337
|
}
|
|
319
338
|
|
|
320
339
|
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,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import { StdioRpcServer } from './stdioRpcServer.js';
|
|
18
|
+
import { createProjectRootGuard } from './projectRootGuard.js';
|
|
18
19
|
|
|
19
20
|
// Deferred state - will be initialized after transport connection
|
|
20
21
|
let unityConnection = null;
|
|
@@ -128,6 +129,15 @@ export async function startServer(options = {}) {
|
|
|
128
129
|
stdioEnabled: options.stdioEnabled !== undefined ? options.stdioEnabled : true
|
|
129
130
|
};
|
|
130
131
|
|
|
132
|
+
const projectInfoProvider =
|
|
133
|
+
deps.projectInfoProvider ||
|
|
134
|
+
(deps.ProjectInfoProvider ? new deps.ProjectInfoProvider() : null);
|
|
135
|
+
const projectRootGuard = createProjectRootGuard({
|
|
136
|
+
requireClientRoot: runtimeConfig.project?.requireClientRoot === true,
|
|
137
|
+
logger,
|
|
138
|
+
projectInfoProvider
|
|
139
|
+
});
|
|
140
|
+
|
|
131
141
|
// Step 2: Create a lightweight stdio MCP server (no TS SDK import)
|
|
132
142
|
const server =
|
|
133
143
|
runtimeConfig.stdioEnabled === false
|
|
@@ -180,7 +190,8 @@ export async function startServer(options = {}) {
|
|
|
180
190
|
port: runtimeConfig.http.port,
|
|
181
191
|
telemetryEnabled: runtimeConfig.telemetry.enabled,
|
|
182
192
|
healthPath: runtimeConfig.http.healthPath,
|
|
183
|
-
allowedHosts: runtimeConfig.http.allowedHosts
|
|
193
|
+
allowedHosts: runtimeConfig.http.allowedHosts,
|
|
194
|
+
requireClientRoot: runtimeConfig.project?.requireClientRoot === true
|
|
184
195
|
});
|
|
185
196
|
try {
|
|
186
197
|
await httpServerInstance.start();
|
|
@@ -362,6 +373,19 @@ export async function startServer(options = {}) {
|
|
|
362
373
|
{ args }
|
|
363
374
|
);
|
|
364
375
|
|
|
376
|
+
const guardError = await projectRootGuard(args || {});
|
|
377
|
+
if (guardError) {
|
|
378
|
+
logger.error(`[MCP] projectRoot guard failed: ${guardError}`);
|
|
379
|
+
return {
|
|
380
|
+
content: [
|
|
381
|
+
{
|
|
382
|
+
type: 'text',
|
|
383
|
+
text: `Error: ${guardError}\nCode: PROJECT_ROOT_MISMATCH`
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
365
389
|
const handler = handlers.get(name);
|
|
366
390
|
if (!handler) {
|
|
367
391
|
logger.error(`[MCP] Tool not found: ${name}`);
|
|
@@ -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;
|