unity-mcp-server 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,38 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * unity-mcp-server – Lightweight MCP server for any Unity project.
4
- * Exposes tools: project info, build scenes, agent docs. No Unity Editor required.
5
- * Set UNITY_PROJECT_PATH to the Unity project root in your MCP client config.
6
- * Run: node dist/index.js (after npm run build).
4
+ * Exposes tools for project info, build scenes, agent docs, packages, code, assets, and more. No Unity Editor required.
7
5
  */
8
6
 
9
- import { readFileSync, existsSync } from "node:fs";
10
- import { join, resolve } from "node:path";
7
+ import { resolve } from "node:path";
11
8
  import { z } from "zod";
9
+ import * as R from "./readers.js";
12
10
 
13
11
  function getProjectRoot(): string {
14
12
  const env = process.env.UNITY_PROJECT_PATH;
15
13
  if (env) return resolve(env);
16
14
  console.error("UNITY_PROJECT_PATH is required. Set it in your MCP client config (e.g. Cursor) to your Unity project root.");
17
15
  process.exit(1);
18
- return ""; // unreachable
19
- }
20
-
21
- function getBuildScenes(projectRoot: string): { index: number; path: string; name: string }[] {
22
- const path = join(projectRoot, "ProjectSettings", "EditorBuildSettings.asset");
23
- if (!existsSync(path)) return [];
24
- const content = readFileSync(path, "utf-8");
25
- const scenes: { index: number; path: string; name: string }[] = [];
26
- let index = 0;
27
- const pathRe = /path: (Assets\/[^\n]+\.unity)/g;
28
- let m: RegExpExecArray | null;
29
- while ((m = pathRe.exec(content)) !== null) {
30
- const fullPath = m[1];
31
- const name = fullPath.replace(/^.*\//, "").replace(/\.unity$/, "");
32
- scenes.push({ index, path: fullPath, name });
33
- index++;
34
- }
35
- return scenes;
16
+ return "";
36
17
  }
37
18
 
38
19
  async function main() {
@@ -46,67 +27,302 @@ async function main() {
46
27
  version: "1.0.0",
47
28
  });
48
29
 
30
+ const text = (s: string) => ({ content: [{ type: "text" as const, text: s }] });
31
+ const json = (o: unknown) => text(JSON.stringify(o, null, 2));
32
+
33
+ // --- Existing + expanded project info ---
49
34
  server.registerTool(
50
35
  "get_project_info",
51
36
  {
52
- description: "Get Unity project info (path, Unity version, build scene count).",
37
+ description: "Get Unity project info: path, Unity version, build scene count, player/product name.",
53
38
  inputSchema: {},
54
39
  },
55
40
  async () => {
56
- let unityVersion = "unknown";
57
- const pvPath = join(projectRoot, "ProjectSettings", "ProjectVersion.txt");
58
- if (existsSync(pvPath)) {
59
- const m = readFileSync(pvPath, "utf-8").match(/m_EditorVersion:\s*(.+)/);
60
- if (m) unityVersion = m[1].trim();
61
- }
62
- const scenes = getBuildScenes(projectRoot);
63
- const text = [
41
+ const unityVersion = R.getUnityVersion(projectRoot);
42
+ const scenes = R.getBuildScenes(projectRoot);
43
+ const player = R.getPlayerSettings(projectRoot);
44
+ const lines = [
64
45
  `Project root: ${projectRoot}`,
65
46
  `Unity version: ${unityVersion}`,
66
47
  `Build scenes: ${scenes.length}`,
67
- scenes.map((s) => ` ${s.index}: ${s.name} (${s.path})`).join("\n"),
68
- ].join("\n");
69
- return { content: [{ type: "text", text }] };
48
+ ...scenes.map((s) => ` ${s.index}: ${s.name} (${s.path})`),
49
+ "",
50
+ "Player / product (if available):",
51
+ ...Object.entries(player).map(([k, v]) => ` ${k}: ${v}`),
52
+ ];
53
+ return text(lines.join("\n"));
70
54
  }
71
55
  );
72
56
 
73
57
  server.registerTool(
74
58
  "list_build_scenes",
59
+ { description: "List scenes in EditorBuildSettings (build order) as JSON.", inputSchema: {} },
60
+ async () => json(R.getBuildScenes(projectRoot))
61
+ );
62
+
63
+ server.registerTool(
64
+ "read_agent_docs",
65
+ {
66
+ description: "Read .agents/AGENT.md and optionally REPO_UNDERSTANDING.md.",
67
+ inputSchema: {
68
+ include_repo_understanding: z.boolean().optional().describe("If true, also return REPO_UNDERSTANDING.md").default(false),
69
+ },
70
+ },
71
+ async (args: unknown) => {
72
+ const include = (args as { include_repo_understanding?: boolean })?.include_repo_understanding ?? false;
73
+ let content = R.readFileSafe(projectRoot, ".agents", "AGENT.md") ?? "(No .agents/AGENT.md found)";
74
+ if (include) {
75
+ const rep = R.readFileSafe(projectRoot, "REPO_UNDERSTANDING.md");
76
+ if (rep) content += "\n\n---\n\n# REPO_UNDERSTANDING.md\n\n" + rep;
77
+ }
78
+ return text(content);
79
+ }
80
+ );
81
+
82
+ // --- 1. Project & package info ---
83
+ server.registerTool(
84
+ "get_player_settings",
85
+ { description: "Get Player/ProjectSettings (product name, company, bundle ID, version, etc.).", inputSchema: {} },
86
+ async () => json(R.getPlayerSettings(projectRoot))
87
+ );
88
+
89
+ server.registerTool(
90
+ "list_packages",
91
+ { description: "List Unity packages from Packages/manifest.json (and optional packages-lock.json).", inputSchema: {} },
92
+ async () => json(R.getPackages(projectRoot))
93
+ );
94
+
95
+ server.registerTool(
96
+ "get_quality_settings",
97
+ { description: "Get QualitySettings (quality levels) from ProjectSettings.", inputSchema: {} },
98
+ async () => json(R.getQualitySettings(projectRoot))
99
+ );
100
+
101
+ server.registerTool(
102
+ "get_scripting_defines",
103
+ { description: "Get global and per-assembly scripting define symbols.", inputSchema: {} },
104
+ async () => json(R.getScriptingDefines(projectRoot))
105
+ );
106
+
107
+ // --- 2. Code & assemblies ---
108
+ server.registerTool(
109
+ "list_assemblies",
75
110
  {
76
- description: "List all scenes in Unity EditorBuildSettings (build order).",
111
+ description: "List assembly definitions (.asmdef) with path, name, references, platforms.",
77
112
  inputSchema: {},
78
113
  },
79
- async () => {
80
- const scenes = getBuildScenes(projectRoot);
81
- const text = JSON.stringify(scenes, null, 2);
82
- return { content: [{ type: "text", text }] };
114
+ async () => json(R.getAssemblyDefinitions(projectRoot))
115
+ );
116
+
117
+ server.registerTool(
118
+ "list_scripts",
119
+ {
120
+ description: "List all C# scripts under Assets. Optionally filter by folder prefix (e.g. Scripts).",
121
+ inputSchema: {
122
+ folder: z.string().optional().describe("Optional folder under Assets, e.g. Scripts/Runtime"),
123
+ },
124
+ },
125
+ async (args: unknown) => json(R.listScripts(projectRoot, (args as { folder?: string })?.folder))
126
+ );
127
+
128
+ server.registerTool(
129
+ "find_scripts_by_content",
130
+ {
131
+ description: "Find C# scripts that contain a type or pattern (e.g. MonoBehaviour, ScriptableObject, or custom). Optionally filter by namespace.",
132
+ inputSchema: {
133
+ pattern: z.string().describe("Pattern to search for, e.g. MonoBehaviour or a type name"),
134
+ namespace_filter: z.string().optional().describe("Optional namespace pattern, e.g. Game.*"),
135
+ },
136
+ },
137
+ async (args: unknown) => {
138
+ const a = args as { pattern: string; namespace_filter?: string };
139
+ return json(R.findScriptsByContent(projectRoot, a.pattern, a.namespace_filter));
83
140
  }
84
141
  );
85
142
 
143
+ // --- 3. Scenes ---
86
144
  server.registerTool(
87
- "read_agent_docs",
145
+ "list_all_scenes",
146
+ { description: "List all .unity scene files under Assets (not only build-indexed).", inputSchema: {} },
147
+ async () => json(R.getAllScenes(projectRoot))
148
+ );
149
+
150
+ server.registerTool(
151
+ "get_scene_summary",
152
+ {
153
+ description: "Get a short summary of a scene: root GameObject names and approximate component count.",
154
+ inputSchema: { scene_path: z.string().describe("Path relative to project, e.g. Assets/Scenes/Main.unity") },
155
+ },
156
+ async (args: unknown) => json(R.getSceneSummary(projectRoot, (args as { scene_path: string }).scene_path))
157
+ );
158
+
159
+ // --- 4. Prefabs ---
160
+ server.registerTool(
161
+ "list_prefabs",
162
+ {
163
+ description: "List all .prefab files. Optionally filter by path prefix under Assets.",
164
+ inputSchema: { path_prefix: z.string().optional().describe("e.g. Prefabs/Characters") },
165
+ },
166
+ async (args: unknown) => json(R.getPrefabs(projectRoot, (args as { path_prefix?: string })?.path_prefix))
167
+ );
168
+
169
+ // --- 5. Assets & references ---
170
+ server.registerTool(
171
+ "find_references",
88
172
  {
89
- description: "Read the agent development guide (.agents/AGENT.md) and optionally REPO_UNDERSTANDING.md.",
173
+ description: "Find all assets (scenes, prefabs, materials, etc.) that reference the given asset. Pass asset path (e.g. Assets/Texture.png) or GUID.",
90
174
  inputSchema: {
91
- include_repo_understanding: z.boolean().optional().describe("If true, also return REPO_UNDERSTANDING.md content").default(false),
175
+ asset_path_or_guid: z.string().describe("Asset path relative to project (e.g. Assets/My.asset) or 32-char GUID"),
92
176
  },
93
177
  },
94
178
  async (args: unknown) => {
95
- const include = (args as { include_repo_understanding?: boolean })?.include_repo_understanding ?? false;
96
- const agentPath = join(projectRoot, ".agents", "AGENT.md");
97
- let text = "";
98
- if (existsSync(agentPath)) {
99
- text = readFileSync(agentPath, "utf-8");
179
+ const input = (args as { asset_path_or_guid: string }).asset_path_or_guid.trim();
180
+ let guid: string;
181
+ if (/^[a-f0-9]{32}$/i.test(input)) {
182
+ guid = input;
100
183
  } else {
101
- text = "(No .agents/AGENT.md found)";
184
+ guid = R.getGuidFromMeta(projectRoot, input) ?? "";
185
+ if (!guid) return text(`No .meta found for ${input}; cannot resolve GUID.`);
102
186
  }
103
- if (include) {
104
- const repPath = join(projectRoot, "REPO_UNDERSTANDING.md");
105
- if (existsSync(repPath)) {
106
- text += "\n\n---\n\n# REPO_UNDERSTANDING.md\n\n" + readFileSync(repPath, "utf-8");
107
- }
108
- }
109
- return { content: [{ type: "text", text }] };
187
+ const refs = R.findReferencesToGuid(projectRoot, guid);
188
+ return json({ guid, references: refs });
189
+ }
190
+ );
191
+
192
+ server.registerTool(
193
+ "get_asset_folder_tree",
194
+ {
195
+ description: "Get a tree of Assets folder (directories and file names). Useful for understanding project layout.",
196
+ inputSchema: { max_depth: z.number().optional().describe("Max folder depth, default 4").default(4) },
197
+ },
198
+ async (args: unknown) => json(R.getAssetFolderTree(projectRoot, (args as { max_depth?: number })?.max_depth ?? 4))
199
+ );
200
+
201
+ server.registerTool(
202
+ "list_assets_by_extension",
203
+ {
204
+ description: "List assets by file extension (e.g. .png, .fbx, .mp3). Optionally under a folder.",
205
+ inputSchema: {
206
+ extension: z.string().describe("e.g. .png, .fbx, .mp3"),
207
+ folder: z.string().optional().describe("Optional folder under Assets"),
208
+ },
209
+ },
210
+ async (args: unknown) => {
211
+ const a = args as { extension: string; folder?: string };
212
+ const ext = a.extension.startsWith(".") ? a.extension : `.${a.extension}`;
213
+ return json(R.listAssetsByExtension(projectRoot, ext, a.folder));
214
+ }
215
+ );
216
+
217
+ // --- 6. Materials & shaders ---
218
+ server.registerTool(
219
+ "list_materials",
220
+ {
221
+ description: "List .mat materials; optionally under a folder. Includes shader GUID when readable.",
222
+ inputSchema: { folder: z.string().optional() },
223
+ },
224
+ async (args: unknown) => json(R.getMaterials(projectRoot, (args as { folder?: string })?.folder))
225
+ );
226
+
227
+ server.registerTool(
228
+ "list_shaders",
229
+ { description: "List all .shader files in Assets and Packages.", inputSchema: {} },
230
+ async () => json(R.getShaders(projectRoot))
231
+ );
232
+
233
+ // --- 7. Animation ---
234
+ server.registerTool(
235
+ "list_animator_controllers",
236
+ { description: "List all Animator Controller (.controller) assets.", inputSchema: {} },
237
+ async () => json(R.getAnimatorControllers(projectRoot))
238
+ );
239
+
240
+ server.registerTool(
241
+ "list_animation_clips",
242
+ { description: "List all Animation Clip (.anim) assets.", inputSchema: {} },
243
+ async () => json(R.getAnimationClips(projectRoot))
244
+ );
245
+
246
+ server.registerTool(
247
+ "get_animator_states",
248
+ {
249
+ description: "Get state names from an Animator Controller (.controller) file.",
250
+ inputSchema: { controller_path: z.string().describe("e.g. Assets/Animations/Player.controller") },
251
+ },
252
+ async (args: unknown) => json(R.getAnimatorStates(projectRoot, (args as { controller_path: string }).controller_path))
253
+ );
254
+
255
+ // --- 8. Audio ---
256
+ server.registerTool(
257
+ "list_audio_clips",
258
+ { description: "List audio clip files (.wav, .mp3, .ogg, .aiff) under Assets.", inputSchema: {} },
259
+ async () => json(R.getAudioClips(projectRoot))
260
+ );
261
+
262
+ server.registerTool(
263
+ "list_audio_mixers",
264
+ { description: "List Audio Mixer (.mixer) assets.", inputSchema: {} },
265
+ async () => json(R.getAudioMixers(projectRoot))
266
+ );
267
+
268
+ // --- 9. Addressables ---
269
+ server.registerTool(
270
+ "get_addressables_info",
271
+ { description: "Get Addressables groups and config path if the project uses Addressables.", inputSchema: {} },
272
+ async () => json(R.getAddressablesInfo(projectRoot))
273
+ );
274
+
275
+ // --- 10. Localization ---
276
+ server.registerTool(
277
+ "get_localization_tables",
278
+ { description: "List localization table files under Assets/Localization if present.", inputSchema: {} },
279
+ async () => json(R.getLocalizationTables(projectRoot))
280
+ );
281
+
282
+ // --- 11. Input ---
283
+ server.registerTool(
284
+ "get_input_axes",
285
+ { description: "Get input axes from InputManager (name, descriptive name, positive button).", inputSchema: {} },
286
+ async () => json(R.getInputAxes(projectRoot))
287
+ );
288
+
289
+ // --- 12. Tags & layers ---
290
+ server.registerTool(
291
+ "get_tags_and_layers",
292
+ { description: "Get Tags and Layers from TagManager.", inputSchema: {} },
293
+ async () => json(R.getTagsAndLayers(projectRoot))
294
+ );
295
+
296
+ // --- 13. Testing ---
297
+ server.registerTool(
298
+ "list_test_assemblies",
299
+ { description: "List assembly definitions that look like test assemblies (path contains test/editor).", inputSchema: {} },
300
+ async () => json(R.getTestAssemblies(projectRoot))
301
+ );
302
+
303
+ // --- 14. Docs & conventions ---
304
+ server.registerTool(
305
+ "get_repo_docs",
306
+ {
307
+ description: "Read README, CONTRIBUTING, .cursorrules, CODING_STANDARDS, STYLE if present at project root.",
308
+ inputSchema: {},
309
+ },
310
+ async () => json(R.getRepoDocs(projectRoot))
311
+ );
312
+
313
+ // --- 15. CI / versioning ---
314
+ server.registerTool(
315
+ "get_project_version",
316
+ { description: "Get project/bundle version from PlayerSettings.", inputSchema: {} },
317
+ async () => text(R.getProjectVersion(projectRoot))
318
+ );
319
+
320
+ server.registerTool(
321
+ "get_changelog",
322
+ { description: "Read CHANGELOG.md or CHANGELOG if present.", inputSchema: {} },
323
+ async () => {
324
+ const content = R.getChangelog(projectRoot);
325
+ return text(content ?? "(No CHANGELOG found)");
110
326
  }
111
327
  );
112
328