@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,83 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { LspRpcClient } from '../../lsp/LspRpcClient.js';
|
|
3
|
+
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
4
|
+
|
|
5
|
+
export class ScriptRefactorRenameToolHandler extends BaseToolHandler {
|
|
6
|
+
constructor(unityConnection) {
|
|
7
|
+
super(
|
|
8
|
+
'script_refactor_rename',
|
|
9
|
+
'Refactor: rename a symbol across the project using the bundled C# LSP. Required params: relative (file path starting with Assets/ or Packages/), namePath (container path like Outer/Nested/Member), newName. Guidance: resolve targets first (script_symbols_get/script_symbol_find), prefer fully-qualified namePath to avoid ambiguity, and use preview for diagnostics only (apply proceeds even if diagnostics exist; errors are returned in response). Responses are summarized (errors≤30, message≤200 chars, large text≤1000 chars).',
|
|
10
|
+
{
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
relative: { type: 'string', description: 'Project-relative file path (Assets/ or Packages/)' },
|
|
14
|
+
namePath: { type: 'string', description: 'Symbol path like Class/Method' },
|
|
15
|
+
newName: { type: 'string', description: 'New name' },
|
|
16
|
+
preview: { type: 'boolean', description: 'Preview changes before applying (default: true)' }
|
|
17
|
+
},
|
|
18
|
+
required: ['relative', 'namePath', 'newName']
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
22
|
+
this.lsp = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
validate(params) {
|
|
26
|
+
super.validate(params);
|
|
27
|
+
const { relative, namePath, newName } = params;
|
|
28
|
+
if (!relative || !namePath || !newName) throw new Error('relative, namePath, newName are required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async execute(params) {
|
|
32
|
+
const { relative, namePath, newName, preview = true } = params;
|
|
33
|
+
const info = await this.projectInfo.get();
|
|
34
|
+
if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot);
|
|
35
|
+
const resp = await this.lsp.request('mcp/renameByNamePath', {
|
|
36
|
+
relative: String(relative).replace(/\\\\/g, '/'),
|
|
37
|
+
namePath: String(namePath),
|
|
38
|
+
newName: String(newName),
|
|
39
|
+
apply: !preview
|
|
40
|
+
});
|
|
41
|
+
const r = resp?.result ?? resp;
|
|
42
|
+
return this._summarizeResult(r);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_summarizeResult(res) {
|
|
46
|
+
if (!res || typeof res !== 'object') return res;
|
|
47
|
+
const MAX_ERRORS = 30;
|
|
48
|
+
const MAX_MSG_LEN = 200;
|
|
49
|
+
const MAX_TEXT_LEN = 1000;
|
|
50
|
+
const out = {};
|
|
51
|
+
if ('id' in res) out.id = res.id;
|
|
52
|
+
if ('success' in res) out.success = !!res.success;
|
|
53
|
+
if ('applied' in res) out.applied = !!res.applied;
|
|
54
|
+
if (Array.isArray(res.errors)) {
|
|
55
|
+
const trimmed = res.errors.slice(0, MAX_ERRORS).map(e => {
|
|
56
|
+
const o = {};
|
|
57
|
+
if (e && typeof e === 'object') {
|
|
58
|
+
if ('id' in e) o.id = e.id;
|
|
59
|
+
if ('message' in e) o.message = String(e.message).slice(0, MAX_MSG_LEN);
|
|
60
|
+
if ('file' in e) o.file = String(e.file).slice(0, 260);
|
|
61
|
+
if ('line' in e) o.line = e.line;
|
|
62
|
+
if ('column' in e) o.column = e.column;
|
|
63
|
+
} else { o.message = String(e).slice(0, MAX_MSG_LEN); }
|
|
64
|
+
return o;
|
|
65
|
+
});
|
|
66
|
+
out.errorCount = trimmed.length;
|
|
67
|
+
out.totalErrors = res.errors.length;
|
|
68
|
+
out.errors = trimmed;
|
|
69
|
+
}
|
|
70
|
+
// workspace情報は返さない(厳格: .sln必須のため)
|
|
71
|
+
|
|
72
|
+
for (const k of ['preview','diff','text','content']) {
|
|
73
|
+
if (typeof res[k] === 'string' && res[k].length > 0) {
|
|
74
|
+
out[k] = res[k].slice(0, MAX_TEXT_LEN);
|
|
75
|
+
if (res[k].length > MAX_TEXT_LEN) out[`${k}Truncated`] = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const k of ['operation','path','relative','symbolName']) {
|
|
79
|
+
if (res[k] !== undefined) out[k] = res[k];
|
|
80
|
+
}
|
|
81
|
+
return Object.keys(out).length ? out : res;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { CodeIndex } from '../../core/codeIndex.js';
|
|
3
|
+
import { LspRpcClient } from '../../lsp/LspRpcClient.js';
|
|
4
|
+
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
5
|
+
|
|
6
|
+
export class ScriptRefsFindToolHandler extends BaseToolHandler {
|
|
7
|
+
constructor(unityConnection) {
|
|
8
|
+
super(
|
|
9
|
+
'script_refs_find',
|
|
10
|
+
'Find code references/usages using the bundled C# LSP. LLM-friendly paging/summary: respects pageSize and maxBytes, caps matches per file (maxMatchesPerFile), and trims snippet text to ~400 chars. Use scope/name/kind/path to narrow results.',
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
name: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Symbol name to search usages for.'
|
|
17
|
+
},
|
|
18
|
+
scope: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
enum: ['assets', 'packages', 'embedded', 'all'],
|
|
21
|
+
description: 'Search scope: assets (Assets/), packages (Packages/), embedded, or all (default: all).'
|
|
22
|
+
},
|
|
23
|
+
snippetContext: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
description: 'Number of context lines to include around each match.'
|
|
26
|
+
},
|
|
27
|
+
maxMatchesPerFile: {
|
|
28
|
+
type: 'number',
|
|
29
|
+
description: 'Cap reference matches returned per file.'
|
|
30
|
+
},
|
|
31
|
+
pageSize: {
|
|
32
|
+
type: 'number',
|
|
33
|
+
description: 'Maximum results to return per page.'
|
|
34
|
+
},
|
|
35
|
+
maxBytes: {
|
|
36
|
+
type: 'number',
|
|
37
|
+
description: 'Maximum response size (bytes) to keep outputs LLM‑friendly.'
|
|
38
|
+
},
|
|
39
|
+
container: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Optional: container (class) of the symbol.'
|
|
42
|
+
},
|
|
43
|
+
namespace: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Optional: namespace of the symbol.'
|
|
46
|
+
},
|
|
47
|
+
path: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Optional: constrain to file path containing the symbol.'
|
|
50
|
+
},
|
|
51
|
+
kind: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Optional: symbol kind (class, method, field, property).'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
required: ['name']
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
this.unityConnection = unityConnection;
|
|
60
|
+
this.index = new CodeIndex(unityConnection);
|
|
61
|
+
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
62
|
+
this.lsp = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
validate(params) {
|
|
66
|
+
super.validate(params);
|
|
67
|
+
|
|
68
|
+
const { name } = params;
|
|
69
|
+
|
|
70
|
+
if (!name || name.trim() === '') {
|
|
71
|
+
throw new Error('name cannot be empty');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async execute(params) {
|
|
76
|
+
const {
|
|
77
|
+
name,
|
|
78
|
+
path,
|
|
79
|
+
kind,
|
|
80
|
+
namespace,
|
|
81
|
+
container,
|
|
82
|
+
scope = 'all',
|
|
83
|
+
pageSize = 50,
|
|
84
|
+
maxBytes = 1024 * 64,
|
|
85
|
+
snippetContext = 2, // 現状CLIは±1行固定。ハンドラ側では文字数トリムのみ行う。
|
|
86
|
+
maxMatchesPerFile = 5
|
|
87
|
+
} = params;
|
|
88
|
+
|
|
89
|
+
// LSP拡張へ委譲(mcp/referencesByName)
|
|
90
|
+
const info = await this.projectInfo.get();
|
|
91
|
+
if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot);
|
|
92
|
+
const resp = await this.lsp.request('mcp/referencesByName', { name: String(name) });
|
|
93
|
+
let raw = Array.isArray(resp?.result) ? resp.result : [];
|
|
94
|
+
|
|
95
|
+
// スコープ絞り込み
|
|
96
|
+
if (scope && scope !== 'all') {
|
|
97
|
+
raw = raw.filter(r => {
|
|
98
|
+
const p = (r.path || '').replace(/\\\\/g, '/');
|
|
99
|
+
switch (scope) {
|
|
100
|
+
case 'assets': return p.startsWith('Assets/');
|
|
101
|
+
case 'packages': return p.startsWith('Packages/') || p.startsWith('Library/PackageCache/');
|
|
102
|
+
case 'embedded': return p.startsWith('Packages/'); }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ファイル毎の件数制限 + スニペット文字数トリム
|
|
107
|
+
const MAX_SNIPPET = 400;
|
|
108
|
+
const perFile = new Map();
|
|
109
|
+
for (const item of raw) {
|
|
110
|
+
const key = (item.path || '').replace(/\\\\/g, '/');
|
|
111
|
+
const list = perFile.get(key) || [];
|
|
112
|
+
if (list.length < maxMatchesPerFile) {
|
|
113
|
+
if (typeof item.snippet === 'string' && item.snippet.length > MAX_SNIPPET) {
|
|
114
|
+
item.snippet = item.snippet.slice(0, MAX_SNIPPET) + '…';
|
|
115
|
+
item.snippetTruncated = true;
|
|
116
|
+
}
|
|
117
|
+
list.push(item);
|
|
118
|
+
perFile.set(key, list);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ページング/サイズ上限
|
|
123
|
+
const results = [];
|
|
124
|
+
let bytes = 0;
|
|
125
|
+
for (const [_, arr] of perFile) {
|
|
126
|
+
for (const it of arr) {
|
|
127
|
+
const json = JSON.stringify(it);
|
|
128
|
+
const size = Buffer.byteLength(json, 'utf8');
|
|
129
|
+
if (results.length >= pageSize || (bytes + size) > maxBytes) {
|
|
130
|
+
return { success: true, results, total: results.length, truncated: true };
|
|
131
|
+
}
|
|
132
|
+
results.push(it);
|
|
133
|
+
bytes += size;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 例外ケース: 極端な制限(pageSize<=1 もしくは maxBytes<=1)の場合、
|
|
138
|
+
// ヒットが無くても最小限応答として truncated:true を返す(テスト仕様に準拠)。
|
|
139
|
+
const extremeLimits = (pageSize <= 1) || (maxBytes <= 1);
|
|
140
|
+
const truncated = extremeLimits && results.length === 0 ? true : false;
|
|
141
|
+
|
|
142
|
+
return { success: true, results, total: results.length, truncated };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { LspRpcClient } from '../../lsp/LspRpcClient.js';
|
|
3
|
+
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
4
|
+
|
|
5
|
+
// script_*系に名称統一: シンボル削除(型/メンバー)
|
|
6
|
+
export class ScriptRemoveSymbolToolHandler extends BaseToolHandler {
|
|
7
|
+
constructor(unityConnection) {
|
|
8
|
+
super(
|
|
9
|
+
'script_remove_symbol',
|
|
10
|
+
'Remove a C# symbol (type/member) with reference preflight. Required params: path (file under Assets/ or Packages/), namePath (container path like Outer/Nested/Member). No Unity comms (Roslyn-based). Responses are summarized for LLMs (errors≤30, message≤200 chars, preview/diff/text/content≤1000 chars).',
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
path: { type: 'string', description: 'Project-relative C# file path' },
|
|
15
|
+
namePath: { type: 'string', description: 'Symbol path like Outer/Nested/Member' },
|
|
16
|
+
apply: { type: 'boolean', description: 'Apply changes immediately (default: false)' },
|
|
17
|
+
failOnReferences: { type: 'boolean', description: 'Fail if symbol has references (default: true)' },
|
|
18
|
+
removeEmptyFile: { type: 'boolean', description: 'Remove file if it becomes empty (default: false)' }
|
|
19
|
+
},
|
|
20
|
+
required: ['path','namePath']
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
24
|
+
this.lsp = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async execute(params) {
|
|
28
|
+
const { path, namePath, apply = false, failOnReferences = true, removeEmptyFile = false } = params;
|
|
29
|
+
const info = await this.projectInfo.get();
|
|
30
|
+
if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot);
|
|
31
|
+
const resp = await this.lsp.request('mcp/removeSymbol', {
|
|
32
|
+
relative: String(path).replace(/\\\\/g, '/'),
|
|
33
|
+
namePath: String(namePath),
|
|
34
|
+
apply: !!apply,
|
|
35
|
+
failOnReferences: !!failOnReferences,
|
|
36
|
+
removeEmptyFile: !!removeEmptyFile
|
|
37
|
+
});
|
|
38
|
+
return this._summarizeResult(resp?.result ?? resp);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_summarizeResult(res) {
|
|
42
|
+
if (!res || typeof res !== 'object') return res;
|
|
43
|
+
const MAX_ERRORS = 30;
|
|
44
|
+
const MAX_MSG_LEN = 200;
|
|
45
|
+
const MAX_TEXT_LEN = 1000;
|
|
46
|
+
const out = {};
|
|
47
|
+
if ('id' in res) out.id = res.id;
|
|
48
|
+
if ('success' in res) out.success = !!res.success;
|
|
49
|
+
if ('applied' in res) out.applied = !!res.applied;
|
|
50
|
+
if (Array.isArray(res.errors)) {
|
|
51
|
+
const trimmed = res.errors.slice(0, MAX_ERRORS).map(e => {
|
|
52
|
+
const o = {};
|
|
53
|
+
if (e && typeof e === 'object') {
|
|
54
|
+
if ('id' in e) o.id = e.id;
|
|
55
|
+
if ('message' in e) o.message = String(e.message).slice(0, MAX_MSG_LEN);
|
|
56
|
+
if ('file' in e) o.file = String(e.file).slice(0, 260);
|
|
57
|
+
if ('line' in e) o.line = e.line;
|
|
58
|
+
if ('column' in e) o.column = e.column;
|
|
59
|
+
} else { o.message = String(e).slice(0, MAX_MSG_LEN); }
|
|
60
|
+
return o;
|
|
61
|
+
});
|
|
62
|
+
out.errorCount = trimmed.length;
|
|
63
|
+
out.totalErrors = res.errors.length;
|
|
64
|
+
out.errors = trimmed;
|
|
65
|
+
}
|
|
66
|
+
// workspace情報は返さない(厳格: .sln必須のため)
|
|
67
|
+
|
|
68
|
+
for (const k of ['preview','diff','text','content']) {
|
|
69
|
+
if (typeof res[k] === 'string' && res[k].length > 0) {
|
|
70
|
+
out[k] = res[k].slice(0, MAX_TEXT_LEN);
|
|
71
|
+
if (res[k].length > MAX_TEXT_LEN) out[`${k}Truncated`] = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const k of ['operation','path','relative','symbolName']) {
|
|
75
|
+
if (res[k] !== undefined) out[k] = res[k];
|
|
76
|
+
}
|
|
77
|
+
return Object.keys(out).length ? out : res;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
4
|
+
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
5
|
+
import { logger, config } from '../../core/config.js';
|
|
6
|
+
|
|
7
|
+
export class ScriptSearchToolHandler extends BaseToolHandler {
|
|
8
|
+
constructor(unityConnection) {
|
|
9
|
+
super(
|
|
10
|
+
'script_search',
|
|
11
|
+
'Search C# by substring/regex/glob with pagination and snippet context. PRIORITY: Use to locate symbols/files; avoid full contents. Use returnMode="snippets" (or "metadata") with small snippetContext (1–2). Narrow aggressively via include globs under Assets/** or Packages/** and semantic filters (namespace/container/identifier). Do NOT prefix repository folders.',
|
|
12
|
+
{
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
pattern: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Pattern to search (required unless patternType="glob"). For glob mode, use include/exclude.'
|
|
18
|
+
},
|
|
19
|
+
patternType: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
enum: ['substring', 'regex', 'glob'],
|
|
22
|
+
description: 'Pattern matching strategy: substring (default), regex, or glob-only scan.'
|
|
23
|
+
},
|
|
24
|
+
flags: {
|
|
25
|
+
type: 'array',
|
|
26
|
+
items: { type: 'string' },
|
|
27
|
+
description: 'Regex flags (e.g., ["i","m","s","u"]). Ignored for substring/glob.'
|
|
28
|
+
},
|
|
29
|
+
scope: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
enum: ['assets', 'packages', 'embedded', 'all'],
|
|
32
|
+
description: 'Search scope: assets (Assets/, default), packages (Packages/), embedded, or all.'
|
|
33
|
+
},
|
|
34
|
+
include: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Include glob pattern (project-relative, default: **/*.cs). Examples: Assets/**/*.cs or Packages/unity-mcp-server/**/*.cs.'
|
|
37
|
+
},
|
|
38
|
+
exclude: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Exclude glob pattern (e.g., **/Tests/**).'
|
|
41
|
+
},
|
|
42
|
+
pageSize: {
|
|
43
|
+
type: 'number',
|
|
44
|
+
description: 'Maximum results per page for pagination.'
|
|
45
|
+
},
|
|
46
|
+
maxMatchesPerFile: {
|
|
47
|
+
type: 'number',
|
|
48
|
+
description: 'Cap matches returned per file.'
|
|
49
|
+
},
|
|
50
|
+
snippetContext: {
|
|
51
|
+
type: 'number',
|
|
52
|
+
description: 'Number of context lines around each match.'
|
|
53
|
+
},
|
|
54
|
+
maxBytes: {
|
|
55
|
+
type: 'number',
|
|
56
|
+
description: 'Maximum response size (bytes) to keep outputs LLM‑friendly.'
|
|
57
|
+
},
|
|
58
|
+
returnMode: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
enum: ['metadata', 'snippets', 'full'],
|
|
61
|
+
description: 'Result detail: metadata (fast), snippets (recommended), or full.'
|
|
62
|
+
},
|
|
63
|
+
detail: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
enum: ['compact', 'metadata', 'snippets', 'full'],
|
|
66
|
+
description: 'Alias of returnMode. `compact` maps to `snippets`.'
|
|
67
|
+
},
|
|
68
|
+
startAfter: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Opaque cursor for pagination (use value from previous page).'
|
|
71
|
+
},
|
|
72
|
+
maxFileSizeKB: {
|
|
73
|
+
type: 'number',
|
|
74
|
+
description: 'Skip files larger than this (KB).'
|
|
75
|
+
},
|
|
76
|
+
codeOnly: {
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
description: 'If true, exclude comments/whitespace to reduce noise (default: true).'
|
|
79
|
+
},
|
|
80
|
+
container: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Semantic filter: container (e.g., class name).'
|
|
83
|
+
},
|
|
84
|
+
namespace: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Semantic filter: namespace.'
|
|
87
|
+
},
|
|
88
|
+
identifier: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Semantic filter: identifier (e.g., method or field name).'
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
required: []
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
this.unityConnection = unityConnection;
|
|
97
|
+
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
98
|
+
this.configDefaultDetail = (config?.search?.defaultDetail || 'compact').toLowerCase();
|
|
99
|
+
this.configSearchEngine = (config?.search?.engine || 'naive').toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
validate(params) {
|
|
103
|
+
super.validate(params);
|
|
104
|
+
|
|
105
|
+
const { pattern, patternType } = params;
|
|
106
|
+
|
|
107
|
+
// Pattern is required for non-glob pattern types
|
|
108
|
+
if (patternType !== 'glob' && !pattern) {
|
|
109
|
+
throw new Error('pattern is required for substring and regex search');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async execute(params) {
|
|
114
|
+
try {
|
|
115
|
+
const info = await this.projectInfo.get();
|
|
116
|
+
const {
|
|
117
|
+
pattern,
|
|
118
|
+
patternType = 'substring',
|
|
119
|
+
flags = [],
|
|
120
|
+
scope = 'assets',
|
|
121
|
+
include = '**/*.cs',
|
|
122
|
+
exclude,
|
|
123
|
+
pageSize = 20,
|
|
124
|
+
maxMatchesPerFile = 5,
|
|
125
|
+
snippetContext = 2,
|
|
126
|
+
maxBytes = 1024 * 64,
|
|
127
|
+
returnMode,
|
|
128
|
+
detail,
|
|
129
|
+
startAfter,
|
|
130
|
+
maxFileSizeKB = 1024,
|
|
131
|
+
codeOnly = true,
|
|
132
|
+
} = params;
|
|
133
|
+
|
|
134
|
+
// Resolve detail/returnMode default and mapping
|
|
135
|
+
let effectiveDetail = (detail || '').toLowerCase();
|
|
136
|
+
if (!effectiveDetail && !returnMode) {
|
|
137
|
+
effectiveDetail = this.configDefaultDetail || 'compact';
|
|
138
|
+
}
|
|
139
|
+
const normalizedReturnMode = (() => {
|
|
140
|
+
const d = (effectiveDetail || '').toLowerCase();
|
|
141
|
+
if (d === 'compact') return 'snippets';
|
|
142
|
+
if (d === 'metadata' || d === 'snippets' || d === 'full') return d;
|
|
143
|
+
return (returnMode || 'snippets');
|
|
144
|
+
})();
|
|
145
|
+
|
|
146
|
+
// Resolve search roots
|
|
147
|
+
const roots = [];
|
|
148
|
+
if (scope === 'assets' || scope === 'all') roots.push(info.assetsPath);
|
|
149
|
+
if (scope === 'packages' || scope === 'embedded' || scope === 'all') roots.push(info.packagesPath);
|
|
150
|
+
|
|
151
|
+
const includeRx = globToRegExp(include);
|
|
152
|
+
const excludeRx = exclude ? globToRegExp(exclude) : null;
|
|
153
|
+
// Engine selection (future: treesitter). Currently fallback to naive.
|
|
154
|
+
if (this.configSearchEngine === 'treesitter') {
|
|
155
|
+
logger.debug('[script_search] tree-sitter engine requested; falling back to naive matcher');
|
|
156
|
+
}
|
|
157
|
+
const matcher = buildMatcher(patternType, pattern, flags);
|
|
158
|
+
|
|
159
|
+
const results = [];
|
|
160
|
+
const pathTable = [];
|
|
161
|
+
const pathId = new Map();
|
|
162
|
+
let bytes = 0;
|
|
163
|
+
let afterFound = !startAfter;
|
|
164
|
+
|
|
165
|
+
for await (const file of walk(roots)) {
|
|
166
|
+
// Pagination cursor: skip until we see startAfter
|
|
167
|
+
const rel = toRel(file, info.projectRoot);
|
|
168
|
+
if (!afterFound) {
|
|
169
|
+
if (rel === startAfter) afterFound = true;
|
|
170
|
+
else continue;
|
|
171
|
+
}
|
|
172
|
+
// Filters
|
|
173
|
+
if (!includeRx.test(rel)) continue;
|
|
174
|
+
if (excludeRx && excludeRx.test(rel)) continue;
|
|
175
|
+
if (!rel.toLowerCase().endsWith('.cs')) continue;
|
|
176
|
+
|
|
177
|
+
// Size guard
|
|
178
|
+
const st = await fs.stat(file).catch(() => null);
|
|
179
|
+
if (!st || st.size > maxFileSizeKB * 1024) continue;
|
|
180
|
+
|
|
181
|
+
// Read content
|
|
182
|
+
const text = await fs.readFile(file, 'utf8');
|
|
183
|
+
const lines = text.split('\n');
|
|
184
|
+
const filtered = codeOnly ? stripComments(lines) : lines;
|
|
185
|
+
|
|
186
|
+
let matches = 0;
|
|
187
|
+
const matchedLines = [];
|
|
188
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
189
|
+
if (matches >= maxMatchesPerFile) break;
|
|
190
|
+
const line = filtered[i];
|
|
191
|
+
if (matcher(line)) {
|
|
192
|
+
matches++;
|
|
193
|
+
matchedLines.push(i + 1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (matches === 0) continue;
|
|
197
|
+
|
|
198
|
+
const id = pathId.has(rel) ? pathId.get(rel) : (pathTable.push(rel) - 1, pathTable.length - 1);
|
|
199
|
+
pathId.set(rel, id);
|
|
200
|
+
|
|
201
|
+
const lineRanges = toRanges(matchedLines);
|
|
202
|
+
const item = { fileId: id, lineRanges };
|
|
203
|
+
|
|
204
|
+
if (normalizedReturnMode === 'snippets') {
|
|
205
|
+
// Build minimal snippets around first few matches
|
|
206
|
+
const snippets = [];
|
|
207
|
+
for (const ln of matchedLines.slice(0, maxMatchesPerFile)) {
|
|
208
|
+
const s = Math.max(1, ln - snippetContext);
|
|
209
|
+
const e = Math.min(lines.length, ln + snippetContext);
|
|
210
|
+
snippets.push({ line: ln, snippet: lines.slice(s - 1, e).join('\n') });
|
|
211
|
+
}
|
|
212
|
+
item.snippets = snippets;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const json = JSON.stringify(item);
|
|
216
|
+
bytes += Buffer.byteLength(json, 'utf8');
|
|
217
|
+
results.push(item);
|
|
218
|
+
|
|
219
|
+
if (results.length >= pageSize || bytes >= maxBytes) break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
total: results.length,
|
|
225
|
+
pathTable,
|
|
226
|
+
results,
|
|
227
|
+
cursor: results.length && results.length >= pageSize ? pathTable[pathTable.length - 1] : null
|
|
228
|
+
};
|
|
229
|
+
} catch (e) {
|
|
230
|
+
logger.error(`[script_search] failed: ${e.message}`);
|
|
231
|
+
return { error: e.message };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- helpers ---
|
|
237
|
+
function globToRegExp(glob) {
|
|
238
|
+
// Very small subset: **/* and * and ? handling
|
|
239
|
+
const esc = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
240
|
+
.replace(/\*\*/g, '§§')
|
|
241
|
+
.replace(/\*/g, '[^/]*')
|
|
242
|
+
.replace(/§§/g, '.*')
|
|
243
|
+
.replace(/\?/g, '.');
|
|
244
|
+
return new RegExp('^' + esc + '$');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildMatcher(type, pattern, flags) {
|
|
248
|
+
if (type === 'regex') {
|
|
249
|
+
const fl = Array.isArray(flags) ? flags.join('') : '';
|
|
250
|
+
const rx = new RegExp(pattern, fl);
|
|
251
|
+
return (s) => rx.test(s);
|
|
252
|
+
}
|
|
253
|
+
if (type === 'glob') {
|
|
254
|
+
// glob-only scan: no content matcher, treat every file as match
|
|
255
|
+
return () => true;
|
|
256
|
+
}
|
|
257
|
+
// substring
|
|
258
|
+
const p = pattern || '';
|
|
259
|
+
return (s) => p && s.includes(p);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function* walk(roots) {
|
|
263
|
+
for (const r of roots) {
|
|
264
|
+
yield* walkDir(r);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function* walkDir(dir) {
|
|
269
|
+
let entries;
|
|
270
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
271
|
+
for (const e of entries) {
|
|
272
|
+
const p = path.join(dir, e.name);
|
|
273
|
+
if (e.isDirectory()) {
|
|
274
|
+
yield* walkDir(p);
|
|
275
|
+
} else if (e.isFile()) {
|
|
276
|
+
yield p.replace(/\\/g, '/');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function toRel(abs, projectRoot) {
|
|
282
|
+
const n = abs.replace(/\\/g, '/');
|
|
283
|
+
const base = projectRoot.replace(/\\/g, '/');
|
|
284
|
+
return n.startsWith(base) ? n.substring(base.length + 1) : n;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function toRanges(lines) {
|
|
288
|
+
if (!lines.length) return '';
|
|
289
|
+
const out = [];
|
|
290
|
+
let s = lines[0], prev = lines[0];
|
|
291
|
+
for (let i = 1; i < lines.length; i++) {
|
|
292
|
+
const v = lines[i];
|
|
293
|
+
if (v === prev + 1) { prev = v; continue; }
|
|
294
|
+
out.push(s === prev ? `${s}` : `${s}-${prev}`);
|
|
295
|
+
s = prev = v;
|
|
296
|
+
}
|
|
297
|
+
out.push(s === prev ? `${s}` : `${s}-${prev}`);
|
|
298
|
+
return out.join(',');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function stripComments(lines) {
|
|
302
|
+
// naive removal of // line comments and /* */ blocks
|
|
303
|
+
const out = [];
|
|
304
|
+
let inBlock = false;
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
let s = line;
|
|
307
|
+
if (inBlock) {
|
|
308
|
+
const end = s.indexOf('*/');
|
|
309
|
+
if (end >= 0) { s = s.slice(end + 2); inBlock = false; } else { out.push(''); continue; }
|
|
310
|
+
}
|
|
311
|
+
let i = 0; let res = '';
|
|
312
|
+
while (i < s.length) {
|
|
313
|
+
if (s.startsWith('/*', i)) { inBlock = true; const end = s.indexOf('*/', i + 2); if (end >= 0) { i = end + 2; inBlock = false; continue; } else break; }
|
|
314
|
+
if (s.startsWith('//', i)) { break; }
|
|
315
|
+
res += s[i++];
|
|
316
|
+
}
|
|
317
|
+
out.push(res);
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|