@akiojin/unity-mcp-server 2.14.14
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/LICENSE +21 -0
- package/README.md +206 -0
- package/bin/unity-mcp-server +2 -0
- package/package.json +73 -0
- package/src/core/codeIndex.js +163 -0
- package/src/core/codeIndexDb.js +96 -0
- package/src/core/config.js +165 -0
- package/src/core/indexWatcher.js +52 -0
- package/src/core/projectInfo.js +111 -0
- package/src/core/server.js +294 -0
- package/src/core/unityConnection.js +426 -0
- package/src/handlers/analysis/AnalyzeSceneContentsToolHandler.js +35 -0
- package/src/handlers/analysis/FindByComponentToolHandler.js +20 -0
- package/src/handlers/analysis/GetAnimatorStateToolHandler.js +37 -0
- package/src/handlers/analysis/GetComponentValuesToolHandler.js +20 -0
- package/src/handlers/analysis/GetGameObjectDetailsToolHandler.js +35 -0
- package/src/handlers/analysis/GetInputActionsStateToolHandler.js +37 -0
- package/src/handlers/analysis/GetObjectReferencesToolHandler.js +20 -0
- package/src/handlers/asset/AssetDatabaseToolHandler.js +221 -0
- package/src/handlers/asset/AssetDependencyToolHandler.js +201 -0
- package/src/handlers/asset/AssetImportSettingsToolHandler.js +170 -0
- package/src/handlers/asset/CreateMaterialToolHandler.js +96 -0
- package/src/handlers/asset/CreatePrefabToolHandler.js +78 -0
- package/src/handlers/asset/ExitPrefabModeToolHandler.js +83 -0
- package/src/handlers/asset/InstantiatePrefabToolHandler.js +133 -0
- package/src/handlers/asset/ModifyMaterialToolHandler.js +76 -0
- package/src/handlers/asset/ModifyPrefabToolHandler.js +72 -0
- package/src/handlers/asset/OpenPrefabToolHandler.js +121 -0
- package/src/handlers/asset/SavePrefabToolHandler.js +106 -0
- package/src/handlers/base/BaseToolHandler.js +133 -0
- package/src/handlers/compilation/GetCompilationStateToolHandler.js +90 -0
- package/src/handlers/component/AddComponentToolHandler.js +126 -0
- package/src/handlers/component/GetComponentTypesToolHandler.js +100 -0
- package/src/handlers/component/ListComponentsToolHandler.js +85 -0
- package/src/handlers/component/ModifyComponentToolHandler.js +143 -0
- package/src/handlers/component/RemoveComponentToolHandler.js +108 -0
- package/src/handlers/console/ClearConsoleToolHandler.js +160 -0
- package/src/handlers/console/ReadConsoleToolHandler.js +276 -0
- package/src/handlers/editor/LayerManagementToolHandler.js +160 -0
- package/src/handlers/editor/SelectionToolHandler.js +141 -0
- package/src/handlers/editor/TagManagementToolHandler.js +129 -0
- package/src/handlers/editor/ToolManagementToolHandler.js +135 -0
- package/src/handlers/editor/WindowManagementToolHandler.js +125 -0
- package/src/handlers/gameobject/CreateGameObjectToolHandler.js +131 -0
- package/src/handlers/gameobject/DeleteGameObjectToolHandler.js +101 -0
- package/src/handlers/gameobject/FindGameObjectToolHandler.js +119 -0
- package/src/handlers/gameobject/GetHierarchyToolHandler.js +132 -0
- package/src/handlers/gameobject/ModifyGameObjectToolHandler.js +128 -0
- package/src/handlers/index.js +389 -0
- package/src/handlers/input/AddInputActionToolHandler.js +20 -0
- package/src/handlers/input/AddInputBindingToolHandler.js +20 -0
- package/src/handlers/input/CreateActionMapToolHandler.js +20 -0
- package/src/handlers/input/CreateCompositeBindingToolHandler.js +20 -0
- package/src/handlers/input/GamepadSimulationHandler.js +116 -0
- package/src/handlers/input/InputSystemHandler.js +80 -0
- package/src/handlers/input/KeyboardSimulationHandler.js +79 -0
- package/src/handlers/input/ManageControlSchemesToolHandler.js +20 -0
- package/src/handlers/input/MouseSimulationHandler.js +107 -0
- package/src/handlers/input/RemoveActionMapToolHandler.js +20 -0
- package/src/handlers/input/RemoveAllBindingsToolHandler.js +20 -0
- package/src/handlers/input/RemoveInputActionToolHandler.js +20 -0
- package/src/handlers/input/RemoveInputBindingToolHandler.js +20 -0
- package/src/handlers/input/TouchSimulationHandler.js +142 -0
- package/src/handlers/menu/ExecuteMenuItemToolHandler.js +304 -0
- package/src/handlers/package/PackageManagerToolHandler.js +248 -0
- package/src/handlers/package/RegistryConfigToolHandler.js +198 -0
- package/src/handlers/playmode/GetEditorStateToolHandler.js +81 -0
- package/src/handlers/playmode/PauseToolHandler.js +44 -0
- package/src/handlers/playmode/PlayToolHandler.js +91 -0
- package/src/handlers/playmode/StopToolHandler.js +77 -0
- package/src/handlers/playmode/WaitForEditorStateToolHandler.js +45 -0
- package/src/handlers/scene/CreateSceneToolHandler.js +91 -0
- package/src/handlers/scene/GetSceneInfoToolHandler.js +20 -0
- package/src/handlers/scene/ListScenesToolHandler.js +58 -0
- package/src/handlers/scene/LoadSceneToolHandler.js +92 -0
- package/src/handlers/scene/SaveSceneToolHandler.js +76 -0
- package/src/handlers/screenshot/AnalyzeScreenshotToolHandler.js +238 -0
- package/src/handlers/screenshot/CaptureScreenshotToolHandler.js +692 -0
- package/src/handlers/script/BuildCodeIndexToolHandler.js +163 -0
- package/src/handlers/script/ScriptCreateClassFileToolHandler.js +60 -0
- package/src/handlers/script/ScriptEditStructuredToolHandler.js +173 -0
- package/src/handlers/script/ScriptIndexStatusToolHandler.js +61 -0
- package/src/handlers/script/ScriptPackagesListToolHandler.js +103 -0
- package/src/handlers/script/ScriptReadToolHandler.js +106 -0
- package/src/handlers/script/ScriptRefactorRenameToolHandler.js +83 -0
- package/src/handlers/script/ScriptRefsFindToolHandler.js +144 -0
- package/src/handlers/script/ScriptRemoveSymbolToolHandler.js +79 -0
- package/src/handlers/script/ScriptSearchToolHandler.js +320 -0
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +117 -0
- package/src/handlers/script/ScriptSymbolsGetToolHandler.js +96 -0
- package/src/handlers/settings/GetProjectSettingsToolHandler.js +161 -0
- package/src/handlers/settings/UpdateProjectSettingsToolHandler.js +272 -0
- package/src/handlers/system/GetCommandStatsToolHandler.js +25 -0
- package/src/handlers/system/PingToolHandler.js +53 -0
- package/src/handlers/system/RefreshAssetsToolHandler.js +45 -0
- package/src/handlers/ui/ClickUIElementToolHandler.js +110 -0
- package/src/handlers/ui/FindUIElementsToolHandler.js +63 -0
- package/src/handlers/ui/GetUIElementStateToolHandler.js +50 -0
- package/src/handlers/ui/SetUIElementValueToolHandler.js +49 -0
- package/src/handlers/ui/SimulateUIInputToolHandler.js +156 -0
- package/src/handlers/video/CaptureVideoForToolHandler.js +96 -0
- package/src/handlers/video/CaptureVideoStartToolHandler.js +38 -0
- package/src/handlers/video/CaptureVideoStatusToolHandler.js +30 -0
- package/src/handlers/video/CaptureVideoStopToolHandler.js +32 -0
- package/src/lsp/CSharpLspUtils.js +134 -0
- package/src/lsp/LspProcessManager.js +60 -0
- package/src/lsp/LspRpcClient.js +133 -0
- package/src/tools/analysis/analyzeSceneContents.js +100 -0
- package/src/tools/analysis/findByComponent.js +87 -0
- package/src/tools/analysis/getAnimatorState.js +326 -0
- package/src/tools/analysis/getComponentValues.js +182 -0
- package/src/tools/analysis/getGameObjectDetails.js +159 -0
- package/src/tools/analysis/getInputActionsState.js +329 -0
- package/src/tools/analysis/getObjectReferences.js +86 -0
- package/src/tools/input/inputActionsEditor.js +556 -0
- package/src/tools/scene/createScene.js +112 -0
- package/src/tools/scene/getSceneInfo.js +95 -0
- package/src/tools/scene/listScenes.js +82 -0
- package/src/tools/scene/loadScene.js +122 -0
- package/src/tools/scene/saveScene.js +91 -0
- package/src/tools/system/ping.js +72 -0
- package/src/tools/video/recordFor.js +31 -0
- package/src/tools/video/recordPlayMode.js +61 -0
- package/src/utils/csharpParse.js +88 -0
- package/src/utils/validators.js +90 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler for querying video capture status in Unity Editor (via MCP)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
5
|
+
|
|
6
|
+
export class CaptureVideoStatusToolHandler extends BaseToolHandler {
|
|
7
|
+
constructor(unityConnection) {
|
|
8
|
+
super(
|
|
9
|
+
'capture_video_status',
|
|
10
|
+
'Get current video recording status.',
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {}
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
this.unityConnection = unityConnection;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @override */
|
|
20
|
+
async execute(params, context) {
|
|
21
|
+
const response = await this.unityConnection.sendCommand('capture_video_status', params || {});
|
|
22
|
+
if (response.error) {
|
|
23
|
+
return { error: response.error, code: response.code || 'UNITY_ERROR' };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...response,
|
|
27
|
+
message: response.message || 'Video recording status retrieved'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler for stopping video capture in Unity Editor (via MCP)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
5
|
+
|
|
6
|
+
export class CaptureVideoStopToolHandler extends BaseToolHandler {
|
|
7
|
+
constructor(unityConnection) {
|
|
8
|
+
super(
|
|
9
|
+
'capture_video_stop',
|
|
10
|
+
'Stop current video recording and finalize the file.',
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
recordingId: { type: 'string', description: 'Optional. Stop a specific recording session. Defaults to the latest.' }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
this.unityConnection = unityConnection;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @override */
|
|
22
|
+
async execute(params, context) {
|
|
23
|
+
const response = await this.unityConnection.sendCommand('capture_video_stop', params || {});
|
|
24
|
+
if (response.error) {
|
|
25
|
+
return { error: response.error, code: response.code || 'UNITY_ERROR' };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...response,
|
|
29
|
+
message: response.message || 'Video recording stopped'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { logger } from '../core/config.js';
|
|
4
|
+
import { WORKSPACE_ROOT } from '../core/config.js';
|
|
5
|
+
|
|
6
|
+
export class CSharpLspUtils {
|
|
7
|
+
constructor() {}
|
|
8
|
+
|
|
9
|
+
detectRid() {
|
|
10
|
+
if (process.platform === 'win32') return process.arch === 'arm64' ? 'win-arm64' : 'win-x64';
|
|
11
|
+
if (process.platform === 'darwin') return process.arch === 'arm64' ? 'osx-arm64' : 'osx-x64';
|
|
12
|
+
return process.arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getDesiredVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve('mcp-server/package.json'), 'utf8'));
|
|
18
|
+
return pkg.version;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getLocalPath(rid) {
|
|
25
|
+
const root = WORKSPACE_ROOT || process.cwd();
|
|
26
|
+
const exe = process.platform === 'win32' ? 'server.exe' : 'server';
|
|
27
|
+
return path.resolve(root, '.unity', 'tools', 'csharp-lsp', rid, exe);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getVersionMarkerPath(rid) {
|
|
31
|
+
const bin = this.getLocalPath(rid);
|
|
32
|
+
return path.resolve(path.dirname(bin), 'VERSION');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
readLocalVersion(rid) {
|
|
36
|
+
try {
|
|
37
|
+
const m = this.getVersionMarkerPath(rid);
|
|
38
|
+
if (fs.existsSync(m)) return fs.readFileSync(m, 'utf8').trim();
|
|
39
|
+
} catch {}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
writeLocalVersion(rid, version) {
|
|
44
|
+
try {
|
|
45
|
+
const m = this.getVersionMarkerPath(rid);
|
|
46
|
+
fs.writeFileSync(m, String(version || '').trim() + '\n', 'utf8');
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async ensureLocal(rid) {
|
|
51
|
+
const p = this.getLocalPath(rid);
|
|
52
|
+
const desired = this.getDesiredVersion();
|
|
53
|
+
if (!desired) throw new Error('mcp-server version not found; cannot resolve LSP tag');
|
|
54
|
+
const current = this.readLocalVersion(rid);
|
|
55
|
+
if (fs.existsSync(p) && current === desired) return p;
|
|
56
|
+
await this.autoDownload(rid, desired);
|
|
57
|
+
if (!fs.existsSync(p)) throw new Error('csharp-lsp binary not found after download');
|
|
58
|
+
this.writeLocalVersion(rid, desired);
|
|
59
|
+
return p;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async autoDownload(rid, version) {
|
|
63
|
+
const repo = process.env.GITHUB_REPOSITORY || 'akiojin/unity-mcp-server';
|
|
64
|
+
const tag = `v${version}`;
|
|
65
|
+
const manifestUrl = `https://github.com/${repo}/releases/download/${tag}/csharp-lsp-manifest.json`;
|
|
66
|
+
const manifest = await this.fetchJson(manifestUrl);
|
|
67
|
+
const entry = manifest?.assets?.[rid];
|
|
68
|
+
if (!entry?.url || !entry?.sha256) throw new Error(`manifest missing entry for ${rid}`);
|
|
69
|
+
|
|
70
|
+
const dest = this.getLocalPath(rid);
|
|
71
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
72
|
+
const tmp = dest + '.download';
|
|
73
|
+
await this.downloadTo(entry.url, tmp);
|
|
74
|
+
const actual = await this.sha256File(tmp);
|
|
75
|
+
if (String(actual).toLowerCase() !== String(entry.sha256).toLowerCase()) {
|
|
76
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
77
|
+
throw new Error('checksum mismatch for csharp-lsp asset');
|
|
78
|
+
}
|
|
79
|
+
// atomic replace
|
|
80
|
+
try { fs.renameSync(tmp, dest); } catch (e) {
|
|
81
|
+
// Windows may need removal before rename
|
|
82
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
83
|
+
fs.renameSync(tmp, dest);
|
|
84
|
+
}
|
|
85
|
+
try { if (process.platform !== 'win32') fs.chmodSync(dest, 0o755); } catch {}
|
|
86
|
+
logger.info(`[csharp-lsp] downloaded: ${path.basename(dest)} @ ${path.dirname(dest)}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async fetchJson(url) {
|
|
90
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'unity-mcp-server' } });
|
|
91
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
92
|
+
return await res.json();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async downloadTo(url, dest) {
|
|
96
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'unity-mcp-server' } });
|
|
97
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
98
|
+
const file = fs.createWriteStream(dest);
|
|
99
|
+
const body = res.body;
|
|
100
|
+
if (body && typeof body.pipe === 'function') {
|
|
101
|
+
await new Promise((resolve, reject) => {
|
|
102
|
+
body.pipe(file);
|
|
103
|
+
body.on('error', reject);
|
|
104
|
+
file.on('finish', resolve);
|
|
105
|
+
file.on('error', reject);
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const { Readable } = await import('node:stream');
|
|
111
|
+
const nodeStream = Readable.fromWeb(body);
|
|
112
|
+
await new Promise((resolve, reject) => {
|
|
113
|
+
nodeStream.pipe(file);
|
|
114
|
+
nodeStream.on('error', reject);
|
|
115
|
+
file.on('finish', resolve);
|
|
116
|
+
file.on('error', reject);
|
|
117
|
+
});
|
|
118
|
+
} catch (e) {
|
|
119
|
+
const ab = await res.arrayBuffer();
|
|
120
|
+
await fs.promises.writeFile(dest, Buffer.from(ab));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async sha256File(file) {
|
|
125
|
+
const { createHash } = await import('crypto');
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const hash = createHash('sha256');
|
|
128
|
+
const stream = fs.createReadStream(file);
|
|
129
|
+
stream.on('data', d => hash.update(d));
|
|
130
|
+
stream.on('error', reject);
|
|
131
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { logger } from '../core/config.js';
|
|
3
|
+
import { CSharpLspUtils } from './CSharpLspUtils.js';
|
|
4
|
+
|
|
5
|
+
export class LspProcessManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.proc = null;
|
|
8
|
+
this.starting = null;
|
|
9
|
+
this.utils = new CSharpLspUtils();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async ensureStarted() {
|
|
13
|
+
if (this.proc && !this.proc.killed) return this.proc;
|
|
14
|
+
if (this.starting) return this.starting;
|
|
15
|
+
this.starting = (async () => {
|
|
16
|
+
const rid = this.utils.detectRid();
|
|
17
|
+
const bin = await this.utils.ensureLocal(rid);
|
|
18
|
+
const proc = spawn(bin, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
19
|
+
proc.on('error', (e) => logger.error(`[csharp-lsp] process error: ${e.message}`));
|
|
20
|
+
proc.on('close', (code, sig) => {
|
|
21
|
+
logger.warn(`[csharp-lsp] exited code=${code} signal=${sig || ''}`);
|
|
22
|
+
this.proc = null;
|
|
23
|
+
});
|
|
24
|
+
proc.stderr.on('data', d => {
|
|
25
|
+
const s = String(d || '').trim();
|
|
26
|
+
if (s) logger.debug(`[csharp-lsp] ${s}`);
|
|
27
|
+
});
|
|
28
|
+
this.proc = proc;
|
|
29
|
+
logger.info(`[csharp-lsp] started (pid=${proc.pid})`);
|
|
30
|
+
return proc;
|
|
31
|
+
})();
|
|
32
|
+
try { return await this.starting; } finally { this.starting = null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async stop(graceMs = 3000) {
|
|
36
|
+
if (!this.proc || this.proc.killed) return;
|
|
37
|
+
const p = this.proc;
|
|
38
|
+
this.proc = null;
|
|
39
|
+
try {
|
|
40
|
+
// Send LSP shutdown/exit if possible
|
|
41
|
+
const shutdown = (obj) => {
|
|
42
|
+
try {
|
|
43
|
+
const json = JSON.stringify(obj);
|
|
44
|
+
const payload = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`;
|
|
45
|
+
p.stdin.write(payload, 'utf8');
|
|
46
|
+
} catch {}
|
|
47
|
+
};
|
|
48
|
+
shutdown({ jsonrpc: '2.0', id: 1, method: 'shutdown', params: {} });
|
|
49
|
+
shutdown({ jsonrpc: '2.0', method: 'exit' });
|
|
50
|
+
p.stdin.end();
|
|
51
|
+
} catch {}
|
|
52
|
+
await new Promise((resolve) => {
|
|
53
|
+
const to = setTimeout(() => {
|
|
54
|
+
try { p.kill('SIGTERM'); } catch {}
|
|
55
|
+
setTimeout(() => { try { p.kill('SIGKILL'); } catch {} ; resolve(); }, 1000);
|
|
56
|
+
}, Math.max(0, graceMs));
|
|
57
|
+
p.on('close', () => { clearTimeout(to); resolve(); });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { LspProcessManager } from './LspProcessManager.js';
|
|
2
|
+
import { config, logger } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
export class LspRpcClient {
|
|
5
|
+
constructor(projectRoot = null) {
|
|
6
|
+
this.mgr = new LspProcessManager();
|
|
7
|
+
this.proc = null;
|
|
8
|
+
this.seq = 1;
|
|
9
|
+
this.pending = new Map();
|
|
10
|
+
this.buf = Buffer.alloc(0);
|
|
11
|
+
this.initialized = false;
|
|
12
|
+
this.projectRoot = projectRoot;
|
|
13
|
+
this.boundOnData = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async ensure() {
|
|
17
|
+
if (this.proc && !this.proc.killed) return this.proc;
|
|
18
|
+
this.proc = await this.mgr.ensureStarted();
|
|
19
|
+
// Attach data handler once per process
|
|
20
|
+
if (this.boundOnData) {
|
|
21
|
+
try { this.proc.stdout.off('data', this.boundOnData); } catch {}
|
|
22
|
+
}
|
|
23
|
+
this.boundOnData = (chunk) => this.onData(chunk);
|
|
24
|
+
this.proc.stdout.on('data', this.boundOnData);
|
|
25
|
+
// On process close: reject all pending and reset state
|
|
26
|
+
this.proc.on('close', () => {
|
|
27
|
+
for (const [id, p] of Array.from(this.pending.entries())) {
|
|
28
|
+
try { p.reject(new Error('LSP process exited')); } catch {}
|
|
29
|
+
this.pending.delete(id);
|
|
30
|
+
}
|
|
31
|
+
this.initialized = false;
|
|
32
|
+
this.proc = null;
|
|
33
|
+
});
|
|
34
|
+
if (!this.initialized) await this.initialize();
|
|
35
|
+
return this.proc;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onData(chunk) {
|
|
39
|
+
this.buf = Buffer.concat([this.buf, Buffer.from(chunk)]);
|
|
40
|
+
while (true) {
|
|
41
|
+
const headerEnd = this.buf.indexOf('\r\n\r\n');
|
|
42
|
+
if (headerEnd < 0) break;
|
|
43
|
+
const header = this.buf.slice(0, headerEnd).toString('utf8');
|
|
44
|
+
const m = header.match(/Content-Length:\s*(\d+)/i);
|
|
45
|
+
const len = m ? parseInt(m[1], 10) : 0;
|
|
46
|
+
const total = headerEnd + 4 + len;
|
|
47
|
+
if (this.buf.length < total) break;
|
|
48
|
+
const jsonBuf = this.buf.slice(headerEnd + 4, total);
|
|
49
|
+
this.buf = this.buf.slice(total);
|
|
50
|
+
try {
|
|
51
|
+
const msg = JSON.parse(jsonBuf.toString('utf8'));
|
|
52
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
53
|
+
this.pending.get(msg.id).resolve(msg);
|
|
54
|
+
this.pending.delete(msg.id);
|
|
55
|
+
}
|
|
56
|
+
} catch { /* ignore */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
writeMessage(obj) {
|
|
61
|
+
const json = JSON.stringify(obj);
|
|
62
|
+
const payload = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`;
|
|
63
|
+
this.proc.stdin.write(payload, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async initialize() {
|
|
67
|
+
await this.ensure();
|
|
68
|
+
const id = this.seq++;
|
|
69
|
+
const req = {
|
|
70
|
+
jsonrpc: '2.0',
|
|
71
|
+
id,
|
|
72
|
+
method: 'initialize',
|
|
73
|
+
params: {
|
|
74
|
+
processId: process.pid,
|
|
75
|
+
rootUri: this.projectRoot ? ('file://' + String(this.projectRoot).replace(/\\/g, '/')) : null,
|
|
76
|
+
capabilities: {},
|
|
77
|
+
workspaceFolders: null,
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const timeoutMs = Math.max(5000, Math.min(60000, config.lsp?.requestTimeoutMs || 60000));
|
|
81
|
+
const p = new Promise((resolve, reject) => {
|
|
82
|
+
this.pending.set(id, { resolve, reject });
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
if (this.pending.has(id)) {
|
|
85
|
+
this.pending.delete(id);
|
|
86
|
+
reject(new Error(`initialize timed out after ${timeoutMs} ms`));
|
|
87
|
+
}
|
|
88
|
+
}, timeoutMs);
|
|
89
|
+
});
|
|
90
|
+
this.writeMessage(req);
|
|
91
|
+
const resp = await p; // ignore result contents for stub
|
|
92
|
+
// send initialized notification
|
|
93
|
+
this.writeMessage({ jsonrpc: '2.0', method: 'initialized', params: {} });
|
|
94
|
+
this.initialized = true;
|
|
95
|
+
return resp;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async request(method, params) {
|
|
99
|
+
return await this.#requestWithRetry(method, params, 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async #requestWithRetry(method, params, attempt) {
|
|
103
|
+
await this.ensure();
|
|
104
|
+
const id = this.seq++;
|
|
105
|
+
const timeoutMs = Math.max(1000, Math.min(300000, config.lsp?.requestTimeoutMs || 60000));
|
|
106
|
+
const p = new Promise((resolve, reject) => {
|
|
107
|
+
this.pending.set(id, { resolve, reject });
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
if (this.pending.has(id)) {
|
|
110
|
+
this.pending.delete(id);
|
|
111
|
+
reject(new Error(`${method} timed out after ${timeoutMs} ms`));
|
|
112
|
+
}
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
this.writeMessage({ jsonrpc: '2.0', id, method, params });
|
|
117
|
+
return await p;
|
|
118
|
+
} catch (e) {
|
|
119
|
+
const msg = String(e && e.message || e);
|
|
120
|
+
const recoverable = /timed out|LSP process exited/i.test(msg);
|
|
121
|
+
if (recoverable && attempt === 1) {
|
|
122
|
+
// Auto-reinit and retry once
|
|
123
|
+
try { await this.mgr.stop(0); } catch {}
|
|
124
|
+
this.proc = null; this.initialized = false; this.buf = Buffer.alloc(0);
|
|
125
|
+
logger.warn(`[csharp-lsp] recoverable error on ${method}: ${msg}. Retrying once...`);
|
|
126
|
+
return await this.#requestWithRetry(method, params, attempt + 1);
|
|
127
|
+
}
|
|
128
|
+
// Standardize error message
|
|
129
|
+
const hint = recoverable ? 'The server was restarted. Try again if the issue persists.' : 'Check request parameters or increase lsp.requestTimeoutMs.';
|
|
130
|
+
throw new Error(`[${method}] failed: ${msg}. ${hint}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool definition for analyze_scene_contents
|
|
3
|
+
*/
|
|
4
|
+
export const analyzeSceneContentsToolDefinition = {
|
|
5
|
+
name: 'analyze_scene_contents',
|
|
6
|
+
description: 'Analyze current scene: object counts, types, prefabs, and memory stats.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
includeInactive: {
|
|
11
|
+
type: 'boolean',
|
|
12
|
+
description: 'Include inactive objects in analysis. Default: true'
|
|
13
|
+
},
|
|
14
|
+
groupByType: {
|
|
15
|
+
type: 'boolean',
|
|
16
|
+
description: 'Group results by component types. Default: true'
|
|
17
|
+
},
|
|
18
|
+
includePrefabInfo: {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
description: 'Include prefab connection info. Default: true'
|
|
21
|
+
},
|
|
22
|
+
includeMemoryInfo: {
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
description: 'Include memory usage estimates. Default: false'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
required: []
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Handler for analyze_scene_contents tool
|
|
33
|
+
*/
|
|
34
|
+
export async function analyzeSceneContentsHandler(unityConnection, args) {
|
|
35
|
+
try {
|
|
36
|
+
// Check connection
|
|
37
|
+
if (!unityConnection.isConnected()) {
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: 'Failed to analyze scene: Unity connection not available'
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
isError: true
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Send command to Unity with provided parameters
|
|
50
|
+
const result = await unityConnection.sendCommand('analyze_scene_contents', args);
|
|
51
|
+
|
|
52
|
+
// The unityConnection.sendCommand already extracts the result field
|
|
53
|
+
// from the response, so we access properties directly on result
|
|
54
|
+
if (!result || typeof result === 'string') {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
text: `Failed to analyze scene: Invalid response format`
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
isError: true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if result has error property (error response from Unity)
|
|
67
|
+
if (result.error) {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: `Failed to analyze scene: ${result.error}`
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
isError: true
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Success response - result is already the unwrapped data
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: result.summary || 'Scene analysis complete'
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
isError: false
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: `Failed to analyze scene: ${error.message}`
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
isError: true
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool definition for find_by_component
|
|
3
|
+
*/
|
|
4
|
+
export const findByComponentToolDefinition = {
|
|
5
|
+
name: 'find_by_component',
|
|
6
|
+
description: 'Find GameObjects that have a specific component type (scene/prefabs/all).',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
componentType: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Component type to search for (e.g., "Light", "Collider", "AudioSource")'
|
|
13
|
+
},
|
|
14
|
+
includeInactive: {
|
|
15
|
+
type: 'boolean',
|
|
16
|
+
description: 'Include inactive GameObjects. Default: true'
|
|
17
|
+
},
|
|
18
|
+
searchScope: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
enum: ['scene', 'prefabs', 'all'],
|
|
21
|
+
description: 'Where to search: current scene, prefabs, or all. Default: "scene"'
|
|
22
|
+
},
|
|
23
|
+
matchExactType: {
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
description: 'Match exact type only (not derived types). Default: true'
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
required: ['componentType']
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handler for find_by_component tool
|
|
34
|
+
*/
|
|
35
|
+
export async function findByComponentHandler(unityConnection, args) {
|
|
36
|
+
try {
|
|
37
|
+
// Check connection
|
|
38
|
+
if (!unityConnection.isConnected()) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: 'Failed to find GameObjects: Unity connection not available'
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
isError: true
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Send command to Unity
|
|
51
|
+
const result = await unityConnection.sendCommand('find_by_component', args);
|
|
52
|
+
|
|
53
|
+
// Handle Unity response
|
|
54
|
+
if (result.status === 'error') {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
text: `Failed to find GameObjects: ${result.error}`
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
isError: true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Success response
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: 'text',
|
|
71
|
+
text: result.result.summary || `Found ${result.result.totalFound} GameObjects`
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
isError: false
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: 'text',
|
|
81
|
+
text: `Failed to find GameObjects: ${error.message}`
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
isError: true
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|