@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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/bin/unity-mcp-server +2 -0
  4. package/package.json +73 -0
  5. package/src/core/codeIndex.js +163 -0
  6. package/src/core/codeIndexDb.js +96 -0
  7. package/src/core/config.js +165 -0
  8. package/src/core/indexWatcher.js +52 -0
  9. package/src/core/projectInfo.js +111 -0
  10. package/src/core/server.js +294 -0
  11. package/src/core/unityConnection.js +426 -0
  12. package/src/handlers/analysis/AnalyzeSceneContentsToolHandler.js +35 -0
  13. package/src/handlers/analysis/FindByComponentToolHandler.js +20 -0
  14. package/src/handlers/analysis/GetAnimatorStateToolHandler.js +37 -0
  15. package/src/handlers/analysis/GetComponentValuesToolHandler.js +20 -0
  16. package/src/handlers/analysis/GetGameObjectDetailsToolHandler.js +35 -0
  17. package/src/handlers/analysis/GetInputActionsStateToolHandler.js +37 -0
  18. package/src/handlers/analysis/GetObjectReferencesToolHandler.js +20 -0
  19. package/src/handlers/asset/AssetDatabaseToolHandler.js +221 -0
  20. package/src/handlers/asset/AssetDependencyToolHandler.js +201 -0
  21. package/src/handlers/asset/AssetImportSettingsToolHandler.js +170 -0
  22. package/src/handlers/asset/CreateMaterialToolHandler.js +96 -0
  23. package/src/handlers/asset/CreatePrefabToolHandler.js +78 -0
  24. package/src/handlers/asset/ExitPrefabModeToolHandler.js +83 -0
  25. package/src/handlers/asset/InstantiatePrefabToolHandler.js +133 -0
  26. package/src/handlers/asset/ModifyMaterialToolHandler.js +76 -0
  27. package/src/handlers/asset/ModifyPrefabToolHandler.js +72 -0
  28. package/src/handlers/asset/OpenPrefabToolHandler.js +121 -0
  29. package/src/handlers/asset/SavePrefabToolHandler.js +106 -0
  30. package/src/handlers/base/BaseToolHandler.js +133 -0
  31. package/src/handlers/compilation/GetCompilationStateToolHandler.js +90 -0
  32. package/src/handlers/component/AddComponentToolHandler.js +126 -0
  33. package/src/handlers/component/GetComponentTypesToolHandler.js +100 -0
  34. package/src/handlers/component/ListComponentsToolHandler.js +85 -0
  35. package/src/handlers/component/ModifyComponentToolHandler.js +143 -0
  36. package/src/handlers/component/RemoveComponentToolHandler.js +108 -0
  37. package/src/handlers/console/ClearConsoleToolHandler.js +160 -0
  38. package/src/handlers/console/ReadConsoleToolHandler.js +276 -0
  39. package/src/handlers/editor/LayerManagementToolHandler.js +160 -0
  40. package/src/handlers/editor/SelectionToolHandler.js +141 -0
  41. package/src/handlers/editor/TagManagementToolHandler.js +129 -0
  42. package/src/handlers/editor/ToolManagementToolHandler.js +135 -0
  43. package/src/handlers/editor/WindowManagementToolHandler.js +125 -0
  44. package/src/handlers/gameobject/CreateGameObjectToolHandler.js +131 -0
  45. package/src/handlers/gameobject/DeleteGameObjectToolHandler.js +101 -0
  46. package/src/handlers/gameobject/FindGameObjectToolHandler.js +119 -0
  47. package/src/handlers/gameobject/GetHierarchyToolHandler.js +132 -0
  48. package/src/handlers/gameobject/ModifyGameObjectToolHandler.js +128 -0
  49. package/src/handlers/index.js +389 -0
  50. package/src/handlers/input/AddInputActionToolHandler.js +20 -0
  51. package/src/handlers/input/AddInputBindingToolHandler.js +20 -0
  52. package/src/handlers/input/CreateActionMapToolHandler.js +20 -0
  53. package/src/handlers/input/CreateCompositeBindingToolHandler.js +20 -0
  54. package/src/handlers/input/GamepadSimulationHandler.js +116 -0
  55. package/src/handlers/input/InputSystemHandler.js +80 -0
  56. package/src/handlers/input/KeyboardSimulationHandler.js +79 -0
  57. package/src/handlers/input/ManageControlSchemesToolHandler.js +20 -0
  58. package/src/handlers/input/MouseSimulationHandler.js +107 -0
  59. package/src/handlers/input/RemoveActionMapToolHandler.js +20 -0
  60. package/src/handlers/input/RemoveAllBindingsToolHandler.js +20 -0
  61. package/src/handlers/input/RemoveInputActionToolHandler.js +20 -0
  62. package/src/handlers/input/RemoveInputBindingToolHandler.js +20 -0
  63. package/src/handlers/input/TouchSimulationHandler.js +142 -0
  64. package/src/handlers/menu/ExecuteMenuItemToolHandler.js +304 -0
  65. package/src/handlers/package/PackageManagerToolHandler.js +248 -0
  66. package/src/handlers/package/RegistryConfigToolHandler.js +198 -0
  67. package/src/handlers/playmode/GetEditorStateToolHandler.js +81 -0
  68. package/src/handlers/playmode/PauseToolHandler.js +44 -0
  69. package/src/handlers/playmode/PlayToolHandler.js +91 -0
  70. package/src/handlers/playmode/StopToolHandler.js +77 -0
  71. package/src/handlers/playmode/WaitForEditorStateToolHandler.js +45 -0
  72. package/src/handlers/scene/CreateSceneToolHandler.js +91 -0
  73. package/src/handlers/scene/GetSceneInfoToolHandler.js +20 -0
  74. package/src/handlers/scene/ListScenesToolHandler.js +58 -0
  75. package/src/handlers/scene/LoadSceneToolHandler.js +92 -0
  76. package/src/handlers/scene/SaveSceneToolHandler.js +76 -0
  77. package/src/handlers/screenshot/AnalyzeScreenshotToolHandler.js +238 -0
  78. package/src/handlers/screenshot/CaptureScreenshotToolHandler.js +692 -0
  79. package/src/handlers/script/BuildCodeIndexToolHandler.js +163 -0
  80. package/src/handlers/script/ScriptCreateClassFileToolHandler.js +60 -0
  81. package/src/handlers/script/ScriptEditStructuredToolHandler.js +173 -0
  82. package/src/handlers/script/ScriptIndexStatusToolHandler.js +61 -0
  83. package/src/handlers/script/ScriptPackagesListToolHandler.js +103 -0
  84. package/src/handlers/script/ScriptReadToolHandler.js +106 -0
  85. package/src/handlers/script/ScriptRefactorRenameToolHandler.js +83 -0
  86. package/src/handlers/script/ScriptRefsFindToolHandler.js +144 -0
  87. package/src/handlers/script/ScriptRemoveSymbolToolHandler.js +79 -0
  88. package/src/handlers/script/ScriptSearchToolHandler.js +320 -0
  89. package/src/handlers/script/ScriptSymbolFindToolHandler.js +117 -0
  90. package/src/handlers/script/ScriptSymbolsGetToolHandler.js +96 -0
  91. package/src/handlers/settings/GetProjectSettingsToolHandler.js +161 -0
  92. package/src/handlers/settings/UpdateProjectSettingsToolHandler.js +272 -0
  93. package/src/handlers/system/GetCommandStatsToolHandler.js +25 -0
  94. package/src/handlers/system/PingToolHandler.js +53 -0
  95. package/src/handlers/system/RefreshAssetsToolHandler.js +45 -0
  96. package/src/handlers/ui/ClickUIElementToolHandler.js +110 -0
  97. package/src/handlers/ui/FindUIElementsToolHandler.js +63 -0
  98. package/src/handlers/ui/GetUIElementStateToolHandler.js +50 -0
  99. package/src/handlers/ui/SetUIElementValueToolHandler.js +49 -0
  100. package/src/handlers/ui/SimulateUIInputToolHandler.js +156 -0
  101. package/src/handlers/video/CaptureVideoForToolHandler.js +96 -0
  102. package/src/handlers/video/CaptureVideoStartToolHandler.js +38 -0
  103. package/src/handlers/video/CaptureVideoStatusToolHandler.js +30 -0
  104. package/src/handlers/video/CaptureVideoStopToolHandler.js +32 -0
  105. package/src/lsp/CSharpLspUtils.js +134 -0
  106. package/src/lsp/LspProcessManager.js +60 -0
  107. package/src/lsp/LspRpcClient.js +133 -0
  108. package/src/tools/analysis/analyzeSceneContents.js +100 -0
  109. package/src/tools/analysis/findByComponent.js +87 -0
  110. package/src/tools/analysis/getAnimatorState.js +326 -0
  111. package/src/tools/analysis/getComponentValues.js +182 -0
  112. package/src/tools/analysis/getGameObjectDetails.js +159 -0
  113. package/src/tools/analysis/getInputActionsState.js +329 -0
  114. package/src/tools/analysis/getObjectReferences.js +86 -0
  115. package/src/tools/input/inputActionsEditor.js +556 -0
  116. package/src/tools/scene/createScene.js +112 -0
  117. package/src/tools/scene/getSceneInfo.js +95 -0
  118. package/src/tools/scene/listScenes.js +82 -0
  119. package/src/tools/scene/loadScene.js +122 -0
  120. package/src/tools/scene/saveScene.js +91 -0
  121. package/src/tools/system/ping.js +72 -0
  122. package/src/tools/video/recordFor.js +31 -0
  123. package/src/tools/video/recordPlayMode.js +61 -0
  124. package/src/utils/csharpParse.js +88 -0
  125. package/src/utils/validators.js +90 -0
@@ -0,0 +1,163 @@
1
+ import { BaseToolHandler } from '../base/BaseToolHandler.js';
2
+ import { CodeIndex } from '../../core/codeIndex.js';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { ProjectInfoProvider } from '../../core/projectInfo.js';
6
+ import { LspRpcClient } from '../../lsp/LspRpcClient.js';
7
+ import { logger } from '../../core/config.js';
8
+
9
+ export class BuildCodeIndexToolHandler extends BaseToolHandler {
10
+ constructor(unityConnection) {
11
+ super(
12
+ 'build_code_index',
13
+ 'Build a persistent SQLite symbol index by scanning document symbols via the C# LSP. Stores DB under Library/UnityMCP/CodeIndex/code-index.db.',
14
+ {
15
+ type: 'object',
16
+ properties: {},
17
+ required: []
18
+ }
19
+ );
20
+ this.unityConnection = unityConnection;
21
+ this.index = new CodeIndex(unityConnection);
22
+ this.projectInfo = new ProjectInfoProvider(unityConnection);
23
+ this.lsp = null; // lazy init with projectRoot
24
+ }
25
+
26
+ async execute(params = {}) {
27
+ try {
28
+ const info = await this.projectInfo.get();
29
+ const roots = [
30
+ path.resolve(info.projectRoot, 'Assets'),
31
+ path.resolve(info.projectRoot, 'Packages'),
32
+ path.resolve(info.projectRoot, 'Library/PackageCache'),
33
+ ];
34
+ const files = [];
35
+ const seen = new Set();
36
+ for (const r of roots) this.walkCs(r, files, seen);
37
+
38
+ if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot);
39
+ const lsp = this.lsp;
40
+
41
+ // Incremental detection based on size-mtime signature
42
+ const makeSig = (abs) => {
43
+ try { const st = fs.statSync(abs); return `${st.size}-${Math.floor(st.mtimeMs)}`; } catch { return '0-0'; }
44
+ };
45
+ const wanted = new Map(files.map(abs => [this.toRel(abs, info.projectRoot), makeSig(abs)]));
46
+ const current = await this.index.getFiles();
47
+ const changed = [];
48
+ const removed = [];
49
+ for (const [rel, sig] of wanted) {
50
+ if (current.get(rel) !== sig) changed.push(rel);
51
+ }
52
+ for (const [rel] of current) if (!wanted.has(rel)) removed.push(rel);
53
+ const toRows = (uri, symbols) => {
54
+ const rel = this.toRel(uri.replace('file://', ''), info.projectRoot);
55
+ const rows = [];
56
+ const visit = (s, container) => {
57
+ const kind = this.kindFromLsp(s.kind);
58
+ const name = s.name || '';
59
+ const start = s.range?.start || s.selectionRange?.start || {};
60
+ rows.push({ path: rel, name, kind, container: container || null, ns: null, line: (start.line ?? 0) + 1, column: (start.character ?? 0) + 1 });
61
+ if (Array.isArray(s.children)) for (const c of s.children) visit(c, name || container);
62
+ };
63
+ if (Array.isArray(symbols)) for (const s of symbols) visit(s, null);
64
+ return rows;
65
+ };
66
+
67
+ // Remove vanished files
68
+ for (const rel of removed) await this.index.removeFile(rel);
69
+
70
+ // Update changed files
71
+ const absList = changed.map(rel => path.resolve(info.projectRoot, rel));
72
+ const concurrency = Math.max(1, Math.min(64, Number(params?.concurrency ?? 8)));
73
+ const reportEvery = Math.max(1, Number(params?.reportEvery ?? 100));
74
+ const startAt = Date.now();
75
+ let i = 0; let updated = 0; let processed = 0;
76
+
77
+ // LSP request with small retry/backoff
78
+ const requestWithRetry = async (uri, maxRetries = Math.max(0, Math.min(5, Number(params?.retry ?? 2)))) => {
79
+ let lastErr = null;
80
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
81
+ try {
82
+ const res = await lsp.request('textDocument/documentSymbol', { textDocument: { uri } });
83
+ return res?.result ?? res;
84
+ } catch (err) {
85
+ lastErr = err;
86
+ await new Promise(r => setTimeout(r, 200 * (attempt + 1)));
87
+ }
88
+ }
89
+ throw lastErr || new Error('documentSymbol failed');
90
+ };
91
+ const worker = async () => {
92
+ while (true) {
93
+ const idx = i++;
94
+ if (idx >= absList.length) break;
95
+ const abs = absList[idx];
96
+ try {
97
+ const uri = 'file://' + abs.replace(/\\/g, '/');
98
+ const docSymbols = await requestWithRetry(uri, 2);
99
+ const rows = toRows(uri, docSymbols);
100
+ const rel = this.toRel(abs, info.projectRoot);
101
+ await this.index.replaceSymbolsForPath(rel, rows);
102
+ await this.index.upsertFile(rel, wanted.get(rel));
103
+ updated += 1;
104
+ } catch {}
105
+ finally {
106
+ processed += 1;
107
+ if (processed % reportEvery === 0 || processed === absList.length) {
108
+ const elapsed = Math.max(1, Date.now() - startAt);
109
+ const rate = (processed * 1000 / elapsed).toFixed(1);
110
+ logger.info(`[index] progress ${processed}/${absList.length} (removed:${removed.length}) rate:${rate} f/s`);
111
+ }
112
+ }
113
+ }
114
+ };
115
+ const workers = Array.from({ length: Math.min(concurrency, absList.length) }, () => worker());
116
+ await Promise.all(workers);
117
+
118
+ const stats = await this.index.getStats();
119
+ return { success: true, updatedFiles: updated, removedFiles: removed.length, totalIndexedSymbols: stats.total, lastIndexedAt: stats.lastIndexedAt };
120
+ } catch (e) {
121
+ return {
122
+ success: false,
123
+ error: 'build_index_failed',
124
+ message: e.message,
125
+ hint: 'C# LSP not ready. Ensure manifest/auto-download and workspace paths are valid.'
126
+ };
127
+ }
128
+ }
129
+
130
+ walkCs(root, files, seen) {
131
+ try {
132
+ if (!fs.existsSync(root)) return;
133
+ const st = fs.statSync(root);
134
+ if (st.isFile()) {
135
+ if (root.endsWith('.cs') && !seen.has(root)) { files.push(root); seen.add(root); }
136
+ return;
137
+ }
138
+ const entries = fs.readdirSync(root, { withFileTypes: true });
139
+ for (const e of entries) {
140
+ if (e.name === 'obj' || e.name === 'bin' || e.name.startsWith('.')) continue;
141
+ this.walkCs(path.join(root, e.name), files, seen);
142
+ }
143
+ } catch {}
144
+ }
145
+
146
+ toRel(full, projectRoot) {
147
+ const normFull = String(full).replace(/\\/g, '/');
148
+ const normRoot = String(projectRoot).replace(/\\/g, '/').replace(/\/$/, '');
149
+ return normFull.startsWith(normRoot) ? normFull.substring(normRoot.length + 1) : normFull;
150
+ }
151
+
152
+ kindFromLsp(k) {
153
+ switch (k) {
154
+ case 5: return 'class';
155
+ case 23: return 'struct';
156
+ case 11: return 'interface';
157
+ case 10: return 'enum';
158
+ case 6: return 'method';
159
+ case 7: return 'property';
160
+ case 8: return 'field';
161
+ case 3: return 'namespace'; }
162
+ }
163
+ }
@@ -0,0 +1,60 @@
1
+ import { BaseToolHandler } from '../base/BaseToolHandler.js';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ // script_*系に名称統一: 新規C#クラスを新規ファイルで生成
6
+ export class ScriptCreateClassFileToolHandler extends BaseToolHandler {
7
+ constructor(unityConnection) {
8
+ super(
9
+ 'script_create_class_file',
10
+ 'Create a new C# class file. Required: path under Assets/ or Packages/ (".cs" appended if missing), className. Optional: namespace, baseType (adds using UnityEngine if MonoBehaviour), usings (CSV), partial. Roslyn preflight (no Unity comms). Responses summarized for LLMs (errors≤30, message≤200 chars, preview≤1000 chars).',
11
+ {
12
+ type: 'object',
13
+ properties: {
14
+ path: { type: 'string', description: 'Project-relative C# file path (e.g., Assets/Scripts/MyClass.cs)' },
15
+ className: { type: 'string', description: 'Class name' },
16
+ namespace: { type: 'string', description: 'Optional namespace' },
17
+ baseType: { type: 'string', description: 'Optional base type (e.g., MonoBehaviour)' },
18
+ usings: { type: 'string', description: 'Comma-separated using directives (e.g., System,Newtonsoft.Json)' },
19
+ partial: { type: 'boolean', description: 'Create as partial class (default: false)' },
20
+ apply: { type: 'boolean', description: 'Apply immediately (default: false)' }
21
+ },
22
+ required: ['path','className']
23
+ }
24
+ );
25
+ this.unityConnection = unityConnection;
26
+ }
27
+
28
+ async execute(params) {
29
+ const { path: rel, className, namespace: ns, baseType, usings, partial = false, apply = false } = params;
30
+ const relative = String(rel).replace(/\\\\/g, '/').endsWith('.cs') ? String(rel).replace(/\\\\/g, '/') : String(rel).replace(/\\\\/g, '/') + '.cs';
31
+ const code = this._buildSource({ className: String(className), ns: ns ? String(ns) : null, baseType: baseType ? String(baseType) : null, usings: usings ? String(usings) : null, partial: !!partial });
32
+ if (!apply) {
33
+ return { success: true, applied: false, preview: code.slice(0, 1000), previewTruncated: code.length > 1000, relative: relative };
34
+ }
35
+ const full = this._resolveFullPath(relative);
36
+ fs.mkdirSync(path.dirname(full), { recursive: true });
37
+ fs.writeFileSync(full, code, 'utf8');
38
+ return { success: true, applied: true, relative };
39
+ }
40
+
41
+ _resolveFullPath(relative) {
42
+ // Project root resolution via ProjectInfoProvider would be heavier; reconstruct by walking up
43
+ const cwd = process.cwd();
44
+ return path.resolve(cwd, relative);
45
+ }
46
+
47
+ _buildSource({ className, ns, baseType, usings, partial }) {
48
+ const useList = [];
49
+ if (usings) useList.push(...usings.split(',').map(s => s.trim()).filter(Boolean));
50
+ if (baseType === 'MonoBehaviour' && !useList.includes('UnityEngine')) useList.push('UnityEngine');
51
+ const header = useList.length ? useList.map(u => `using ${u};`).join('\n') + '\n\n' : '';
52
+ const partialKw = partial ? ' partial' : '';
53
+ const baseClause = baseType ? ` : ${baseType}` : '';
54
+ const classBlock = `public${partialKw} class ${className}${baseClause}\n{\n}\n`;
55
+ if (ns) {
56
+ return `${header}namespace ${ns}\n{\n${classBlock}\n}`;
57
+ }
58
+ return `${header}${classBlock}`;
59
+ }
60
+ }
@@ -0,0 +1,173 @@
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 ScriptEditStructuredToolHandler extends BaseToolHandler {
6
+ constructor(unityConnection) {
7
+ super(
8
+ 'script_edit_structured',
9
+ 'PRIORITY: Use this for ALL code changes. Perform structured edits (insert_before/insert_after/replace_body) on a known symbol. Required params: path (file under Assets/ or Packages/), symbolName (prefer container path e.g. MyType/Nested/Foo). Guidance: (1) Locate targets with script_symbols_get first, (2) insert_* must target a class/namespace (never a method), (3) replace_body must be a self-contained body including braces, (4) preview is for diagnostics only — apply proceeds even if diagnostics exist; errors are returned. LLM summary limits: errors≤30 (message≤200 chars, file≤260), large text (preview/diff/text/content)≤1000 chars with Truncated flag.',
10
+ {
11
+ type: 'object',
12
+ properties: {
13
+ operation: {
14
+ type: 'string',
15
+ enum: ['insert_before', 'insert_after', 'replace_body'],
16
+ description: 'Edit type: insert_before, insert_after, or replace_body.'
17
+ },
18
+ path: {
19
+ type: 'string',
20
+ description: 'Project-relative C# path starting with Assets/ or Packages/ (e.g., Packages/unity-mcp-server/Editor/Foo.cs). Do NOT prefix repository folders like UnityMCPServer/….'
21
+ },
22
+ symbolName: {
23
+ type: 'string',
24
+ description: 'Target symbol name (e.g., class/method/field name).'
25
+ },
26
+ kind: {
27
+ type: 'string',
28
+ description: 'Symbol kind (e.g., class, method, field, property). Optional but improves precision.'
29
+ },
30
+ newText: {
31
+ type: 'string',
32
+ description: 'Text to insert or use as replacement body.'
33
+ },
34
+ preview: {
35
+ type: 'boolean',
36
+ description: 'If true, returns a preview without writing files. Default=false to reduce large diff payloads.'
37
+ }
38
+ },
39
+ required: ['operation', 'path', 'symbolName']
40
+ }
41
+ );
42
+ this.unityConnection = unityConnection;
43
+ this.projectInfo = new ProjectInfoProvider(unityConnection);
44
+ this.lsp = null;
45
+ }
46
+
47
+ validate(params) {
48
+ super.validate(params);
49
+
50
+ const { operation, path, symbolName, kind } = params;
51
+
52
+ const validOperations = ['insert_before', 'insert_after', 'replace_body'];
53
+ if (!validOperations.includes(operation)) {
54
+ throw new Error(`Invalid operation: ${operation}`);
55
+ }
56
+
57
+ if (!path || path.trim() === '') {
58
+ throw new Error('path cannot be empty');
59
+ }
60
+
61
+ if (!symbolName || symbolName.trim() === '') {
62
+ throw new Error('symbolName cannot be empty');
63
+ }
64
+
65
+ // Safety guard: forbid inserting members into a method scope
66
+ if ((operation === 'insert_after' || operation === 'insert_before') && (kind || '').toLowerCase() === 'method') {
67
+ throw new Error('Insert operations must target class/namespace, not method scope. Use kind:"class" and insert at class level.');
68
+ }
69
+ }
70
+
71
+ async execute(params) {
72
+ // Normalize to project-relative path
73
+ const raw = String(params.path).replace(/\\\\/g, '/');
74
+ const ai = raw.indexOf('Assets/');
75
+ const pi = raw.indexOf('Packages/');
76
+ const idx = (ai >= 0 && pi >= 0) ? Math.min(ai, pi) : (ai >= 0 ? ai : pi);
77
+ const relative = idx >= 0 ? raw.substring(idx) : raw;
78
+
79
+ const operation = String(params.operation);
80
+ const kind = (params.kind || '').toLowerCase();
81
+ const symbolName = String(params.symbolName);
82
+ const preview = params?.preview === true;
83
+ const body = String(params.newText || '');
84
+
85
+ // Map operations to LSP extensions
86
+ const info = await this.projectInfo.get();
87
+ if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot);
88
+
89
+ if (operation === 'replace_body') {
90
+ const resp = await this.lsp.request('mcp/replaceSymbolBody', {
91
+ relative,
92
+ namePath: symbolName,
93
+ body,
94
+ apply: !preview
95
+ });
96
+ return this._summarizeResult(resp?.result ?? resp, { preview });
97
+ }
98
+ if (operation === 'insert_before' || operation === 'insert_after') {
99
+ const method = operation === 'insert_before' ? 'mcp/insertBeforeSymbol' : 'mcp/insertAfterSymbol';
100
+ const resp = await this.lsp.request(method, {
101
+ relative,
102
+ namePath: symbolName,
103
+ text: body,
104
+ apply: !preview
105
+ });
106
+ return this._summarizeResult(resp?.result ?? resp, { preview });
107
+ }
108
+
109
+ return { error: `Unsupported operation: ${operation}` };
110
+ }
111
+
112
+ /**
113
+ * Summarize/trim responses to avoid huge token usage.
114
+ * - Caps error items and message lengths
115
+ * - Trims large text fields (e.g., preview/diff) to a short excerpt
116
+ */
117
+ _summarizeResult(res, { preview }) {
118
+ if (!res || typeof res !== 'object') return res;
119
+
120
+ const MAX_ERRORS = 30;
121
+ const MAX_MSG_LEN = 200;
122
+ const MAX_TEXT_LEN = 1000; // generic cap for any preview-like text
123
+
124
+ const out = {};
125
+ // Preserve common flags if present
126
+ if ('id' in res) out.id = res.id;
127
+ if ('success' in res) out.success = !!res.success;
128
+ if ('applied' in res) out.applied = !!res.applied;
129
+
130
+ // Errors trimming
131
+ if (Array.isArray(res.errors)) {
132
+ const trimmed = res.errors.slice(0, MAX_ERRORS).map(e => {
133
+ const obj = {};
134
+ if (e && typeof e === 'object') {
135
+ if ('id' in e) obj.id = e.id;
136
+ if ('message' in e) obj.message = this._trimString(String(e.message), MAX_MSG_LEN);
137
+ if ('file' in e) obj.file = this._trimString(String(e.file), 260);
138
+ if ('line' in e) obj.line = e.line;
139
+ if ('column' in e) obj.column = e.column;
140
+ } else {
141
+ obj.message = this._trimString(String(e), MAX_MSG_LEN);
142
+ }
143
+ return obj;
144
+ });
145
+ out.errorCount = trimmed.length; // summarized count (<= MAX_ERRORS)
146
+ out.totalErrors = res.errors.length; // raw count for reference
147
+ out.errors = trimmed;
148
+ }
149
+
150
+ // Propagate workspace info if present (which .sln/.csproj is open)
151
+ // workspace情報は返さない(厳格: .sln必須のため)
152
+
153
+ // Generic handling for any large text properties commonly returned by tools
154
+ for (const key of ['preview', 'diff', 'text', 'content']) {
155
+ if (typeof res[key] === 'string' && res[key].length > 0) {
156
+ out[key] = this._trimString(res[key], MAX_TEXT_LEN);
157
+ if (res[key].length > MAX_TEXT_LEN) out[`${key}Truncated`] = true;
158
+ }
159
+ }
160
+
161
+ // Echo minimal identifiers to aid clients
162
+ for (const key of ['operation', 'path', 'relative', 'symbolName']) {
163
+ if (res[key] !== undefined) out[key] = res[key];
164
+ }
165
+
166
+ return Object.keys(out).length ? out : res;
167
+ }
168
+
169
+ _trimString(s, max) {
170
+ if (typeof s !== 'string') return s;
171
+ return s.length > max ? (s.slice(0, max) + '…') : s;
172
+ }
173
+ }
@@ -0,0 +1,61 @@
1
+ import { BaseToolHandler } from '../base/BaseToolHandler.js';
2
+ import { ProjectInfoProvider } from '../../core/projectInfo.js';
3
+
4
+ export class ScriptIndexStatusToolHandler extends BaseToolHandler {
5
+ constructor(unityConnection) {
6
+ super(
7
+ 'script_index_status',
8
+ 'Report code index status and readiness for symbol/search operations. BEST PRACTICES: Check before heavy symbol operations. Shows total files indexed and coverage percentage. If coverage is low, some symbol operations may be incomplete. Index is automatically built on first use. No parameters needed - lightweight status check.',
9
+ {
10
+ type: 'object',
11
+ properties: {},
12
+ required: []
13
+ }
14
+ );
15
+ this.unityConnection = unityConnection;
16
+ this.projectInfo = new ProjectInfoProvider(unityConnection);
17
+ }
18
+
19
+ async execute(params) {
20
+ // まず永続インデックスの有無を確認。未構築なら明示エラーで build_code_index を促す。
21
+ const { CodeIndex } = await import('../../core/codeIndex.js');
22
+ const idx = new CodeIndex(this.unityConnection);
23
+ const ready = await idx.isReady();
24
+ if (!ready) {
25
+ return { success: false, error: 'index_not_built', message: 'Code index is not built. Please run UnityMCP.build_code_index first.' };
26
+ }
27
+
28
+ // 構築済みなら .cs 総数をローカル走査で取得(軽量)
29
+ const info = await this.projectInfo.get();
30
+ const fs = await import('fs');
31
+ const path = await import('path');
32
+ const roots = [
33
+ path.default.resolve(info.projectRoot, 'Assets'),
34
+ path.default.resolve(info.projectRoot, 'Packages'),
35
+ path.default.resolve(info.projectRoot, 'Library/PackageCache'),
36
+ ];
37
+ let total = 0; const breakdown = { assets: 0, packages: 0, packageCache: 0, other: 0 };
38
+ const walk = (dir) => {
39
+ try {
40
+ if (!fs.default.existsSync(dir)) return;
41
+ const st = fs.default.statSync(dir);
42
+ if (st.isFile()) {
43
+ if (dir.endsWith('.cs')) {
44
+ total++;
45
+ const rel = dir.replace(/\\\\/g, '/').replace(info.projectRoot.replace(/\\\\/g,'/'), '').replace(/^\//,'');
46
+ if (rel.startsWith('Assets/')) breakdown.assets++; else if (rel.startsWith('Packages/')) breakdown.packages++; else if (rel.includes('Library/PackageCache/')) breakdown.packageCache++; else breakdown.other++;
47
+ }
48
+ return;
49
+ }
50
+ for (const e of fs.default.readdirSync(dir)) {
51
+ if (e === 'obj' || e === 'bin' || e.startsWith('.')) continue;
52
+ walk(path.default.join(dir, e));
53
+ }
54
+ } catch {}
55
+ };
56
+ for (const r of roots) walk(r);
57
+ const stats = await idx.getStats();
58
+ const coverage = total > 0 ? Math.min(1, stats.total / total) : 0;
59
+ return { success: true, totalFiles: total, indexedFiles: stats.total, coverage, breakdown, index: { ready: true, rows: stats.total, lastIndexedAt: stats.lastIndexedAt } };
60
+ }
61
+ }
@@ -0,0 +1,103 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { BaseToolHandler } from '../base/BaseToolHandler.js';
4
+ import { ProjectInfoProvider } from '../../core/projectInfo.js';
5
+ import { logger } from '../../core/config.js';
6
+
7
+ export class ScriptPackagesListToolHandler extends BaseToolHandler {
8
+ constructor(unityConnection) {
9
+ super(
10
+ 'script_packages_list',
11
+ 'List Unity packages in the project (optionally include built‑in). BEST PRACTICES: Use to discover available packages and their paths. Set includeBuiltIn=false to see only user packages. Returns package IDs, versions, and resolved paths. Embedded packages can be edited directly. Essential for understanding project dependencies.',
12
+ {
13
+ type: 'object',
14
+ properties: {
15
+ includeBuiltIn: {
16
+ type: 'boolean',
17
+ description: 'If true, includes built‑in packages in results (default: false).'
18
+ }
19
+ },
20
+ required: []
21
+ }
22
+ );
23
+ this.unityConnection = unityConnection;
24
+ this.projectInfo = new ProjectInfoProvider(unityConnection);
25
+ }
26
+
27
+ async execute(params) {
28
+ const { includeBuiltIn = false } = params;
29
+ const info = await this.projectInfo.get();
30
+
31
+ // Prefer packages-lock.json for authoritative list (includes builtin/embedded/registry)
32
+ const lockPath = path.join(info.projectRoot, 'Packages', 'packages-lock.json');
33
+ const manifestPath = path.join(info.projectRoot, 'Packages', 'manifest.json');
34
+ let results = [];
35
+ try {
36
+ if (fs.existsSync(lockPath)) {
37
+ const json = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
38
+ const deps = json?.dependencies || {};
39
+ for (const [name, meta] of Object.entries(deps)) {
40
+ const source = String(meta.source || '').toLowerCase();
41
+ if (!includeBuiltIn && source === 'builtin') continue;
42
+ const version = String(meta.version || '');
43
+ let resolvedPath = null;
44
+ let isEmbedded = source === 'embedded';
45
+ if (isEmbedded) {
46
+ // For embedded, manifest specifies file:folder
47
+ try {
48
+ const man = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
49
+ const spec = man?.dependencies?.[name];
50
+ if (typeof spec === 'string' && spec.startsWith('file:')) {
51
+ const folder = spec.substring('file:'.length);
52
+ resolvedPath = path.join(info.projectRoot, 'Packages', folder).replace(/\\/g, '/');
53
+ }
54
+ } catch {}
55
+ }
56
+ results.push({
57
+ packageId: `${name}@${version}`,
58
+ name,
59
+ displayName: name,
60
+ version,
61
+ source,
62
+ isEmbedded,
63
+ resolvedPath
64
+ });
65
+ }
66
+ } else if (fs.existsSync(manifestPath)) {
67
+ const man = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
68
+ const deps = man?.dependencies || {};
69
+ for (const [name, spec] of Object.entries(deps)) {
70
+ let source = 'registry';
71
+ let version = String(spec);
72
+ let resolvedPath = null;
73
+ let isEmbedded = false;
74
+ if (version.startsWith('file:')) {
75
+ source = 'embedded';
76
+ isEmbedded = true;
77
+ const folder = version.substring('file:'.length);
78
+ resolvedPath = path.join(info.projectRoot, 'Packages', folder).replace(/\\/g, '/');
79
+ }
80
+ if (!includeBuiltIn && source === 'builtin') continue;
81
+ results.push({
82
+ packageId: `${name}@${version}`,
83
+ name,
84
+ displayName: name,
85
+ version,
86
+ source,
87
+ isEmbedded,
88
+ resolvedPath
89
+ });
90
+ }
91
+ } else {
92
+ return { success: true, packages: [], totalCount: 0 };
93
+ }
94
+ } catch (e) {
95
+ logger.error(`[script_packages_list] local parse failed: ${e.message}`);
96
+ return { error: e.message };
97
+ }
98
+
99
+ // Sort by name
100
+ results.sort((a, b) => a.name.localeCompare(b.name));
101
+ return { success: true, packages: results, totalCount: results.length };
102
+ }
103
+ }
@@ -0,0 +1,106 @@
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 } from '../../core/config.js';
6
+
7
+ export class ScriptReadToolHandler extends BaseToolHandler {
8
+ constructor(unityConnection) {
9
+ super(
10
+ 'script_read',
11
+ 'Read a C# file with optional line range and payload limits. Files must be under Assets/ or Packages/ and have .cs extension. PRIORITY: Read minimally — locate the target with script_symbols_get and read only the signature area (~30–40 lines). For large files, always pass startLine/endLine and (optionally) maxBytes.',
12
+ {
13
+ type: 'object',
14
+ properties: {
15
+ path: {
16
+ type: 'string',
17
+ description: 'Project-relative C# path under Assets/ or Packages/ (e.g., Packages/unity-mcp-server/Editor/Example.cs). Do NOT prefix repository folders (e.g., UnityMCPServer/…).'
18
+ },
19
+ startLine: {
20
+ type: 'number',
21
+ description: 'Starting line (1-based, inclusive). Defaults to 1.'
22
+ },
23
+ endLine: {
24
+ type: 'number',
25
+ description: 'Ending line (inclusive). Defaults to startLine + 199 when omitted.'
26
+ },
27
+ maxBytes: {
28
+ type: 'number',
29
+ description: 'Maximum bytes to return to cap payload size for LLMs.'
30
+ }
31
+ },
32
+ required: ['path']
33
+ }
34
+ );
35
+ this.unityConnection = unityConnection;
36
+ this.projectInfo = new ProjectInfoProvider(unityConnection);
37
+ }
38
+
39
+ validate(params) {
40
+ super.validate(params);
41
+
42
+ const { path, startLine, endLine } = params;
43
+
44
+ // Validate path
45
+ if (!path || path.trim() === '') {
46
+ throw new Error('path cannot be empty');
47
+ }
48
+
49
+ // Validate line numbers if provided
50
+ if (startLine !== undefined && startLine < 1) {
51
+ throw new Error('startLine must be at least 1');
52
+ }
53
+
54
+ if (endLine !== undefined && startLine !== undefined && endLine < startLine) {
55
+ throw new Error('endLine cannot be less than startLine');
56
+ }
57
+ }
58
+
59
+ async execute(params) {
60
+ const {
61
+ path,
62
+ startLine = 1,
63
+ endLine,
64
+ maxBytes
65
+ } = params;
66
+
67
+ try {
68
+ // Resolve project paths (Unity未接続でも推定可)
69
+ const info = await this.projectInfo.get();
70
+
71
+ // Normalize and validate
72
+ // Normalize common mistakes like UnityMCPServer/Packages/… → Packages/…
73
+ const raw = (path || '').replace(/\\/g, '/');
74
+ const ai = raw.indexOf('Assets/');
75
+ const pi = raw.indexOf('Packages/');
76
+ const idx = (ai >= 0 && pi >= 0) ? Math.min(ai, pi) : (ai >= 0 ? ai : pi);
77
+ const norm = idx >= 0 ? raw.substring(idx) : raw;
78
+ if (!norm.startsWith('Assets/') && !norm.startsWith('Packages/')) {
79
+ return { error: 'Path must be under Assets/ or Packages/' };
80
+ }
81
+ if (!norm.toLowerCase().endsWith('.cs')) {
82
+ return { error: 'Only .cs files are supported' };
83
+ }
84
+
85
+ const abs = info.projectRoot + '/' + norm;
86
+ const stat = await fs.stat(abs).catch(() => null);
87
+ if (!stat || !stat.isFile()) return { error: 'File not found', path: norm };
88
+
89
+ const data = await fs.readFile(abs, 'utf8');
90
+ const lines = data.split('\n');
91
+ const s = Math.max(1, startLine);
92
+ const e = Math.min(lines.length, endLine || (s + 199));
93
+ let content = lines.slice(s - 1, e).join('\n');
94
+
95
+ if (typeof maxBytes === 'number' && maxBytes > 0) {
96
+ const buf = Buffer.from(content, 'utf8');
97
+ if (buf.length > maxBytes) content = buf.subarray(0, maxBytes).toString('utf8');
98
+ }
99
+
100
+ return { success: true, path: norm, startLine: s, endLine: e, content };
101
+ } catch (e) {
102
+ logger.error(`[script_read] failed: ${e.message}`);
103
+ return { error: e.message };
104
+ }
105
+ }
106
+ }