@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.
- package/.github/workflows/npm-publish.yml +34 -0
- package/.mcpbignore +9 -0
- package/CHANGELOG.md +85 -0
- package/LICENSE +69 -0
- package/README.md +368 -0
- package/claude-desktop-config.json +12 -0
- package/docs/unity-mcp-architecture.gif +0 -0
- package/docs/unity-mcp-features.gif +0 -0
- package/docs/unity-mcp-showcase-brickbreaker.gif +0 -0
- package/docs/unity-mcp-showcase-castle.gif +0 -0
- package/docs/unity-mcp-showcase-village.gif +0 -0
- package/icon.png +0 -0
- package/manifest.json +178 -0
- package/package.json +26 -0
- package/src/config.js +52 -0
- package/src/index.js +529 -0
- package/src/instance-discovery.js +501 -0
- package/src/state-persistence.js +97 -0
- package/src/tool-tiers.js +348 -0
- package/src/tools/context-tools.js +33 -0
- package/src/tools/editor-tools.js +4521 -0
- package/src/tools/hub-tools.js +96 -0
- package/src/tools/instance-tools.js +114 -0
- package/src/tools/uma-tools.js +627 -0
- package/src/uma-bridge.js +63 -0
- package/src/unity-editor-bridge.js +1690 -0
- package/src/unity-hub.js +125 -0
- package/tests/multi-agent-stress-test.mjs +400 -0
|
@@ -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
|
+
];
|