@mseep/anklebreaker-unity-mcp 2.30.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.
@@ -0,0 +1,348 @@
1
+ // AnkleBreaker Unity MCP — Two-tier tool system
2
+ // Reduces the exposed tool count to avoid overwhelming MCP clients.
3
+ //
4
+ // Core tools: Always exposed as individual MCP tools (~60 tools)
5
+ // Advanced tools: Accessed via unity_advanced_tool (200+ tools)
6
+ //
7
+ // Why: MCP clients like Claude Cowork silently fail when a server
8
+ // exposes too many tools (our 268 tools / 125KB response was ~5x
9
+ // larger than working servers). This keeps us under the safe limit.
10
+ //
11
+ // Lazy loading: Advanced tools support dynamic dispatch. If a tool
12
+ // isn't in the cached map, the route is derived from the tool name
13
+ // (unity_terrain_list → terrain/list) and called directly via sendCommand.
14
+ // This means new tools added to the C# plugin work immediately without
15
+ // restarting the MCP server.
16
+
17
+ import { sendCommand } from "./unity-editor-bridge.js";
18
+
19
+ /**
20
+ * Explicit route overrides for tools whose API endpoints
21
+ * don't follow the standard name → route derivation pattern.
22
+ * E.g. unity_mppm_* tools use "scenario/*" endpoints on the C# side.
23
+ */
24
+ const ROUTE_OVERRIDES = {
25
+ unity_mppm_list_scenarios: "scenario/list",
26
+ unity_mppm_status: "scenario/status",
27
+ unity_mppm_activate_scenario: "scenario/activate",
28
+ unity_mppm_start: "scenario/start",
29
+ unity_mppm_stop: "scenario/stop",
30
+ unity_mppm_info: "scenario/info",
31
+ unity_mppm_list_players: "mppm/list-players",
32
+ unity_mppm_activate_player: "mppm/activate-player",
33
+ unity_mppm_deactivate_player: "mppm/deactivate-player",
34
+ };
35
+
36
+ /**
37
+ * Derive an HTTP route from a tool name.
38
+ * unity_terrain_raise_lower → terrain/raise-lower
39
+ * unity_animation_create_clip → animation/create-clip
40
+ */
41
+ function toolNameToRoute(toolName) {
42
+ // Check explicit overrides first (for tools whose API routes don't match their name)
43
+ if (ROUTE_OVERRIDES[toolName]) return ROUTE_OVERRIDES[toolName];
44
+
45
+ // Remove unity_ prefix
46
+ const withoutPrefix = toolName.replace(/^unity_/, "");
47
+ // Split into parts: first part is category, rest is action
48
+ const parts = withoutPrefix.split("_");
49
+ if (parts.length < 2) return null;
50
+ const category = parts[0];
51
+ const action = parts.slice(1).join("-");
52
+ return `${category}/${action}`;
53
+ }
54
+
55
+ // ─── Core tool names (always exposed individually) ───
56
+ const CORE_TOOLS = new Set([
57
+ // Connection & state
58
+ "unity_editor_ping",
59
+ "unity_editor_state",
60
+ "unity_project_info",
61
+
62
+ // Scene management
63
+ "unity_scene_info",
64
+ "unity_scene_open",
65
+ "unity_scene_save",
66
+ "unity_scene_new",
67
+ "unity_scene_hierarchy",
68
+ "unity_scene_stats",
69
+
70
+ // GameObject CRUD
71
+ "unity_gameobject_create",
72
+ "unity_gameobject_delete",
73
+ "unity_gameobject_info",
74
+ "unity_gameobject_set_transform",
75
+ "unity_gameobject_duplicate",
76
+ "unity_gameobject_set_active",
77
+ "unity_gameobject_reparent",
78
+
79
+ // Component management
80
+ "unity_component_add",
81
+ "unity_component_remove",
82
+ "unity_component_get_properties",
83
+ "unity_component_set_property",
84
+ "unity_component_set_reference",
85
+ "unity_component_batch_wire",
86
+ "unity_component_get_referenceable",
87
+
88
+ // Asset management
89
+ "unity_asset_list",
90
+ "unity_asset_import",
91
+ "unity_asset_delete",
92
+ "unity_asset_create_prefab",
93
+ "unity_asset_instantiate_prefab",
94
+
95
+ // Script management
96
+ "unity_script_create",
97
+ "unity_script_read",
98
+ "unity_script_update",
99
+ "unity_execute_code",
100
+
101
+ // Material
102
+ "unity_material_create",
103
+ "unity_renderer_set_material",
104
+
105
+ // Build & play
106
+ "unity_build",
107
+ "unity_play_mode",
108
+
109
+ // Console & Compilation
110
+ "unity_console_log",
111
+ "unity_console_clear",
112
+ "unity_get_compilation_errors",
113
+
114
+ // Editor actions
115
+ "unity_execute_menu_item",
116
+ "unity_undo",
117
+ "unity_redo",
118
+ "unity_undo_history",
119
+
120
+ // Selection & search
121
+ "unity_selection_get",
122
+ "unity_selection_set",
123
+ "unity_selection_focus_scene_view",
124
+ "unity_selection_find_by_type",
125
+ "unity_search_by_component",
126
+ "unity_search_by_tag",
127
+ "unity_search_by_layer",
128
+ "unity_search_by_name",
129
+ "unity_search_assets",
130
+ "unity_search_missing_references",
131
+
132
+ // Screenshots & capture
133
+ "unity_screenshot_game",
134
+ "unity_screenshot_scene",
135
+ "unity_screenshot_editor_window",
136
+ "unity_graphics_scene_capture",
137
+ "unity_graphics_game_capture",
138
+
139
+ // Prefab basics
140
+ "unity_prefab_info",
141
+ "unity_set_object_reference",
142
+
143
+ // Packages
144
+ "unity_packages_list",
145
+ "unity_packages_add",
146
+ "unity_packages_remove",
147
+ "unity_packages_search",
148
+ "unity_packages_info",
149
+
150
+ // Queue & agents
151
+ "unity_queue_info",
152
+ "unity_agents_list",
153
+ "unity_agent_log",
154
+ ]);
155
+
156
+ /**
157
+ * Split a flat tool array into { core, advanced }.
158
+ * Also generates the meta-tools for accessing advanced tools.
159
+ */
160
+ export function splitToolTiers(allEditorTools) {
161
+ const core = [];
162
+ const advanced = [];
163
+
164
+ for (const tool of allEditorTools) {
165
+ if (CORE_TOOLS.has(tool.name)) {
166
+ core.push(tool);
167
+ } else {
168
+ advanced.push(tool);
169
+ }
170
+ }
171
+
172
+ // Build an index of advanced tools for the catalog
173
+ const advancedIndex = advanced.map((t) => ({
174
+ name: t.name,
175
+ description: t.description,
176
+ }));
177
+
178
+ // Group advanced tools by category for the catalog
179
+ const categories = {};
180
+ for (const t of advanced) {
181
+ // Extract category from tool name: unity_animation_create_clip → animation
182
+ const parts = t.name.replace(/^unity_/, "").split("_");
183
+ const cat = parts[0];
184
+ if (!categories[cat]) categories[cat] = [];
185
+ categories[cat].push(t.name);
186
+ }
187
+
188
+ // Build the handler map for quick lookup
189
+ const advancedMap = new Map();
190
+ for (const t of advanced) {
191
+ advancedMap.set(t.name, t);
192
+ }
193
+
194
+ // ─── Meta-tools ───
195
+
196
+ const catalogTool = {
197
+ name: "unity_list_advanced_tools",
198
+ description:
199
+ "List all available advanced/specialized Unity tools organized by category. " +
200
+ "These tools are not directly exposed but can be called via unity_advanced_tool. " +
201
+ "Categories include: uma, animation, prefab, physics, lighting, audio, shadergraph, " +
202
+ "amplify, terrain, particle, navmesh, ui, texture, profiler, memory, settings, " +
203
+ "input, asmdef, scriptableobject, constraint, lod, editorprefs, playerprefs, " +
204
+ "vfx, graphics, sceneview, and more.",
205
+ inputSchema: {
206
+ type: "object",
207
+ properties: {
208
+ category: {
209
+ type: "string",
210
+ description:
211
+ 'Filter by category name (e.g. "animation", "prefab", "shadergraph"). Omit for full list.',
212
+ },
213
+ },
214
+ },
215
+ handler: async ({ category } = {}) => {
216
+ // Try to fetch dynamic routes from Unity plugin for lazy discovery
217
+ let dynamicRoutes = null;
218
+ try {
219
+ dynamicRoutes = await sendCommand("_meta/routes", {});
220
+ } catch (_) {
221
+ // Plugin might not support _meta/routes yet, use cached list only
222
+ }
223
+
224
+ // Merge dynamic routes into the advanced tool list
225
+ // Dynamic routes that aren't in our cached map get listed as lazy-loadable tools
226
+ let mergedCategories = { ...categories };
227
+ let dynamicCount = 0;
228
+
229
+ if (dynamicRoutes && dynamicRoutes.routes) {
230
+ for (const route of dynamicRoutes.routes) {
231
+ // Convert route to tool name: terrain/list → unity_terrain_list
232
+ const toolName = "unity_" + route.replace(/\//g, "_").replace(/-/g, "_");
233
+ const cat = route.split("/")[0];
234
+
235
+ // Skip if already in our cached map
236
+ if (advancedMap.has(toolName) || CORE_TOOLS.has(toolName)) continue;
237
+
238
+ // Add to merged categories
239
+ if (!mergedCategories[cat]) mergedCategories[cat] = [];
240
+ if (!mergedCategories[cat].includes(toolName)) {
241
+ mergedCategories[cat].push(toolName);
242
+ dynamicCount++;
243
+ }
244
+ }
245
+ }
246
+
247
+ if (category) {
248
+ const cat = category.toLowerCase();
249
+
250
+ // Check cached tools first
251
+ const matching = advanced.filter((t) => {
252
+ const toolCat = t.name.replace(/^unity_/, "").split("_")[0];
253
+ return toolCat === cat;
254
+ });
255
+
256
+ // Also include dynamic-only tools for this category
257
+ const dynamicTools = (mergedCategories[cat] || [])
258
+ .filter((name) => !advancedMap.has(name))
259
+ .map((name) => ({ name, description: `(lazy-loaded from Unity plugin)` }));
260
+
261
+ const all = [
262
+ ...matching.map((t) => ({ name: t.name, description: t.description })),
263
+ ...dynamicTools,
264
+ ];
265
+
266
+ if (all.length === 0) {
267
+ return `No advanced tools found for category "${category}". Available categories: ${Object.keys(mergedCategories).join(", ")}`;
268
+ }
269
+ return JSON.stringify(all, null, 2);
270
+ }
271
+
272
+ // Full catalog grouped by category
273
+ const result = {};
274
+ for (const [cat, names] of Object.entries(mergedCategories)) {
275
+ result[cat] = names;
276
+ }
277
+ return JSON.stringify(
278
+ {
279
+ totalAdvancedTools: advanced.length + dynamicCount,
280
+ dynamicTools: dynamicCount,
281
+ categories: result,
282
+ },
283
+ null,
284
+ 2
285
+ );
286
+ },
287
+ };
288
+
289
+ const advancedTool = {
290
+ name: "unity_advanced_tool",
291
+ description:
292
+ "Execute an advanced/specialized Unity tool by name. Use unity_list_advanced_tools " +
293
+ "to discover available tools and their parameters. This provides access to 200+ " +
294
+ "specialized tools for animation, prefabs, physics, shaders, terrain, particles, " +
295
+ "UI, profiling, and more.",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ tool: {
300
+ type: "string",
301
+ description:
302
+ 'The tool name to execute (e.g. "unity_animation_create_controller", "unity_shadergraph_create")',
303
+ },
304
+ params: {
305
+ type: "object",
306
+ description:
307
+ "Parameters to pass to the tool. Use unity_list_advanced_tools to see required parameters.",
308
+ additionalProperties: true,
309
+ },
310
+ },
311
+ required: ["tool"],
312
+ },
313
+ handler: async ({ tool, params } = {}) => {
314
+ if (!tool) {
315
+ return "Error: 'tool' parameter is required. Use unity_list_advanced_tools to see available tools.";
316
+ }
317
+
318
+ const targetTool = advancedMap.get(tool);
319
+ if (targetTool) {
320
+ return await targetTool.handler(params || {});
321
+ }
322
+
323
+ // ─── Lazy loading fallback ───
324
+ // Tool not in cached map — derive the route from the name and call Unity directly.
325
+ // This allows new tools added to the C# plugin to work without restarting the MCP server.
326
+ const route = toolNameToRoute(tool);
327
+ if (route) {
328
+ try {
329
+ // Log to stderr, not stdout — stdout carries the MCP JSON-RPC transport.
330
+ console.error(`[MCP] Lazy-loading tool "${tool}" via route "${route}"`);
331
+ const result = await sendCommand(route, params || {});
332
+ return JSON.stringify(result, null, 2);
333
+ } catch (err) {
334
+ return `Error executing "${tool}" (lazy route: ${route}): ${err.message}`;
335
+ }
336
+ }
337
+
338
+ return `Error: Unknown tool "${tool}". Use unity_list_advanced_tools to see available tools.`;
339
+ },
340
+ };
341
+
342
+ return {
343
+ coreTools: core,
344
+ metaTools: [catalogTool, advancedTool],
345
+ advancedCount: advanced.length,
346
+ coreCount: core.length,
347
+ };
348
+ }
@@ -0,0 +1,33 @@
1
+ // AnkleBreaker Unity MCP — Tool definitions for Project Context
2
+ // These tools give agents access to project-specific documentation and guidelines
3
+ // stored in the Unity project's Assets/MCP/Context/ folder.
4
+
5
+ import * as bridge from "../unity-editor-bridge.js";
6
+
7
+ export const contextTools = [
8
+ {
9
+ name: "unity_get_project_context",
10
+ description:
11
+ "Get project-specific context and documentation that the team has prepared for AI agents. " +
12
+ "This includes project guidelines, architecture docs, game design documents, networking rules, " +
13
+ "and any other project knowledge stored in Assets/MCP/Context/. " +
14
+ "Call this without arguments to get ALL context, or specify a category for a specific document. " +
15
+ "IMPORTANT: Call this early in your session to understand the project's conventions and architecture.",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ category: {
20
+ type: "string",
21
+ description:
22
+ "Optional: specific context category to fetch (e.g. 'ProjectGuidelines', 'Architecture', " +
23
+ "'GameDesign', 'NetworkingGuidelines', 'NetworkingCSP', or any custom category). " +
24
+ "Omit to get all available context.",
25
+ },
26
+ },
27
+ },
28
+ handler: async ({ category } = {}) => {
29
+ const data = await bridge.getProjectContext(category || null);
30
+ return JSON.stringify(data, null, 2);
31
+ },
32
+ },
33
+ ];