rbxstudio-mcp 2.3.1 → 2.4.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.
Files changed (175) hide show
  1. package/README.md +67 -14
  2. package/dist/__tests__/bridge-service.test.js +25 -13
  3. package/dist/__tests__/bridge-service.test.js.map +1 -1
  4. package/dist/__tests__/bridge-session.test.d.ts +2 -0
  5. package/dist/__tests__/bridge-session.test.d.ts.map +1 -0
  6. package/dist/__tests__/bridge-session.test.js +171 -0
  7. package/dist/__tests__/bridge-session.test.js.map +1 -0
  8. package/dist/__tests__/chunker.test.d.ts +2 -0
  9. package/dist/__tests__/chunker.test.d.ts.map +1 -0
  10. package/dist/__tests__/chunker.test.js +201 -0
  11. package/dist/__tests__/chunker.test.js.map +1 -0
  12. package/dist/__tests__/docs-core.test.d.ts +2 -0
  13. package/dist/__tests__/docs-core.test.d.ts.map +1 -0
  14. package/dist/__tests__/docs-core.test.js +137 -0
  15. package/dist/__tests__/docs-core.test.js.map +1 -0
  16. package/dist/__tests__/docs-fetcher.test.d.ts +2 -0
  17. package/dist/__tests__/docs-fetcher.test.d.ts.map +1 -0
  18. package/dist/__tests__/docs-fetcher.test.js +173 -0
  19. package/dist/__tests__/docs-fetcher.test.js.map +1 -0
  20. package/dist/__tests__/helpers.d.ts +8 -0
  21. package/dist/__tests__/helpers.d.ts.map +1 -0
  22. package/dist/__tests__/helpers.js +23 -0
  23. package/dist/__tests__/helpers.js.map +1 -0
  24. package/dist/__tests__/http-routes.test.d.ts +2 -0
  25. package/dist/__tests__/http-routes.test.d.ts.map +1 -0
  26. package/dist/__tests__/http-routes.test.js +233 -0
  27. package/dist/__tests__/http-routes.test.js.map +1 -0
  28. package/dist/__tests__/http-server.test.js +13 -6
  29. package/dist/__tests__/http-server.test.js.map +1 -1
  30. package/dist/__tests__/integration.test.js +9 -4
  31. package/dist/__tests__/integration.test.js.map +1 -1
  32. package/dist/__tests__/semantic-search.test.d.ts +2 -0
  33. package/dist/__tests__/semantic-search.test.d.ts.map +1 -0
  34. package/dist/__tests__/semantic-search.test.js +202 -0
  35. package/dist/__tests__/semantic-search.test.js.map +1 -0
  36. package/dist/__tests__/smoke.test.js +7 -3
  37. package/dist/__tests__/smoke.test.js.map +1 -1
  38. package/dist/__tests__/studio-client.test.d.ts +2 -0
  39. package/dist/__tests__/studio-client.test.d.ts.map +1 -0
  40. package/dist/__tests__/studio-client.test.js +25 -0
  41. package/dist/__tests__/studio-client.test.js.map +1 -0
  42. package/dist/__tests__/tool-nudges.test.d.ts +2 -0
  43. package/dist/__tests__/tool-nudges.test.d.ts.map +1 -0
  44. package/dist/__tests__/tool-nudges.test.js +60 -0
  45. package/dist/__tests__/tool-nudges.test.js.map +1 -0
  46. package/dist/__tests__/tool-registry.test.d.ts +2 -0
  47. package/dist/__tests__/tool-registry.test.d.ts.map +1 -0
  48. package/dist/__tests__/tool-registry.test.js +365 -0
  49. package/dist/__tests__/tool-registry.test.js.map +1 -0
  50. package/dist/__tests__/tools-bridge.test.d.ts +2 -0
  51. package/dist/__tests__/tools-bridge.test.d.ts.map +1 -0
  52. package/dist/__tests__/tools-bridge.test.js +396 -0
  53. package/dist/__tests__/tools-bridge.test.js.map +1 -0
  54. package/dist/__tests__/tools-docs.test.d.ts +2 -0
  55. package/dist/__tests__/tools-docs.test.d.ts.map +1 -0
  56. package/dist/__tests__/tools-docs.test.js +112 -0
  57. package/dist/__tests__/tools-docs.test.js.map +1 -0
  58. package/dist/__tests__/tools-guards.test.d.ts +2 -0
  59. package/dist/__tests__/tools-guards.test.d.ts.map +1 -0
  60. package/dist/__tests__/tools-guards.test.js +131 -0
  61. package/dist/__tests__/tools-guards.test.js.map +1 -0
  62. package/dist/__tests__/tools-runtime.test.d.ts +2 -0
  63. package/dist/__tests__/tools-runtime.test.d.ts.map +1 -0
  64. package/dist/__tests__/tools-runtime.test.js +214 -0
  65. package/dist/__tests__/tools-runtime.test.js.map +1 -0
  66. package/dist/__tests__/tools-visual.test.d.ts +2 -0
  67. package/dist/__tests__/tools-visual.test.d.ts.map +1 -0
  68. package/dist/__tests__/tools-visual.test.js +149 -0
  69. package/dist/__tests__/tools-visual.test.js.map +1 -0
  70. package/dist/bridge-service.d.ts +99 -12
  71. package/dist/bridge-service.d.ts.map +1 -1
  72. package/dist/bridge-service.js +238 -21
  73. package/dist/bridge-service.js.map +1 -1
  74. package/dist/docs/cache.d.ts +50 -0
  75. package/dist/docs/cache.d.ts.map +1 -0
  76. package/dist/docs/cache.js +123 -0
  77. package/dist/docs/cache.js.map +1 -0
  78. package/dist/docs/embeddings/chunker.d.ts +120 -0
  79. package/dist/docs/embeddings/chunker.d.ts.map +1 -0
  80. package/dist/docs/embeddings/chunker.js +395 -0
  81. package/dist/docs/embeddings/chunker.js.map +1 -0
  82. package/dist/docs/embeddings/embedder.d.ts +41 -0
  83. package/dist/docs/embeddings/embedder.d.ts.map +1 -0
  84. package/dist/docs/embeddings/embedder.js +113 -0
  85. package/dist/docs/embeddings/embedder.js.map +1 -0
  86. package/dist/docs/embeddings/index.d.ts +102 -0
  87. package/dist/docs/embeddings/index.d.ts.map +1 -0
  88. package/dist/docs/embeddings/index.js +250 -0
  89. package/dist/docs/embeddings/index.js.map +1 -0
  90. package/dist/docs/embeddings/manager.d.ts +68 -0
  91. package/dist/docs/embeddings/manager.d.ts.map +1 -0
  92. package/dist/docs/embeddings/manager.js +97 -0
  93. package/dist/docs/embeddings/manager.js.map +1 -0
  94. package/dist/docs/fetcher.d.ts +29 -0
  95. package/dist/docs/fetcher.d.ts.map +1 -0
  96. package/dist/docs/fetcher.js +244 -0
  97. package/dist/docs/fetcher.js.map +1 -0
  98. package/dist/docs/reference.d.ts +37 -0
  99. package/dist/docs/reference.d.ts.map +1 -0
  100. package/dist/docs/reference.js +108 -0
  101. package/dist/docs/reference.js.map +1 -0
  102. package/dist/docs/search.d.ts +194 -0
  103. package/dist/docs/search.d.ts.map +1 -0
  104. package/dist/docs/search.js +733 -0
  105. package/dist/docs/search.js.map +1 -0
  106. package/dist/http-server.d.ts.map +1 -1
  107. package/dist/http-server.js +52 -5
  108. package/dist/http-server.js.map +1 -1
  109. package/dist/index.d.ts +8 -9
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +35 -1035
  112. package/dist/index.js.map +1 -1
  113. package/dist/instructions.d.ts +15 -0
  114. package/dist/instructions.d.ts.map +1 -0
  115. package/dist/instructions.js +26 -0
  116. package/dist/instructions.js.map +1 -0
  117. package/dist/tools/defs/attributes.d.ts +6 -0
  118. package/dist/tools/defs/attributes.d.ts.map +1 -0
  119. package/dist/tools/defs/attributes.js +85 -0
  120. package/dist/tools/defs/attributes.js.map +1 -0
  121. package/dist/tools/defs/docs.d.ts +17 -0
  122. package/dist/tools/defs/docs.d.ts.map +1 -0
  123. package/dist/tools/defs/docs.js +151 -0
  124. package/dist/tools/defs/docs.js.map +1 -0
  125. package/dist/tools/defs/execute.d.ts +6 -0
  126. package/dist/tools/defs/execute.d.ts.map +1 -0
  127. package/dist/tools/defs/execute.js +21 -0
  128. package/dist/tools/defs/execute.js.map +1 -0
  129. package/dist/tools/defs/inspection.d.ts +7 -0
  130. package/dist/tools/defs/inspection.d.ts.map +1 -0
  131. package/dist/tools/defs/inspection.js +202 -0
  132. package/dist/tools/defs/inspection.js.map +1 -0
  133. package/dist/tools/defs/objects.d.ts +6 -0
  134. package/dist/tools/defs/objects.d.ts.map +1 -0
  135. package/dist/tools/defs/objects.js +111 -0
  136. package/dist/tools/defs/objects.js.map +1 -0
  137. package/dist/tools/defs/properties.d.ts +6 -0
  138. package/dist/tools/defs/properties.d.ts.map +1 -0
  139. package/dist/tools/defs/properties.js +71 -0
  140. package/dist/tools/defs/properties.js.map +1 -0
  141. package/dist/tools/defs/runtime.d.ts +6 -0
  142. package/dist/tools/defs/runtime.d.ts.map +1 -0
  143. package/dist/tools/defs/runtime.js +145 -0
  144. package/dist/tools/defs/runtime.js.map +1 -0
  145. package/dist/tools/defs/scripts.d.ts +18 -0
  146. package/dist/tools/defs/scripts.d.ts.map +1 -0
  147. package/dist/tools/defs/scripts.js +163 -0
  148. package/dist/tools/defs/scripts.js.map +1 -0
  149. package/dist/tools/defs/tags.d.ts +6 -0
  150. package/dist/tools/defs/tags.d.ts.map +1 -0
  151. package/dist/tools/defs/tags.js +74 -0
  152. package/dist/tools/defs/tags.js.map +1 -0
  153. package/dist/tools/defs/visual.d.ts +7 -0
  154. package/dist/tools/defs/visual.d.ts.map +1 -0
  155. package/dist/tools/defs/visual.js +208 -0
  156. package/dist/tools/defs/visual.js.map +1 -0
  157. package/dist/tools/index.d.ts +101 -25
  158. package/dist/tools/index.d.ts.map +1 -1
  159. package/dist/tools/index.js +580 -63
  160. package/dist/tools/index.js.map +1 -1
  161. package/dist/tools/nudges.d.ts +25 -0
  162. package/dist/tools/nudges.d.ts.map +1 -0
  163. package/dist/tools/nudges.js +34 -0
  164. package/dist/tools/nudges.js.map +1 -0
  165. package/dist/tools/registry.d.ts +20 -0
  166. package/dist/tools/registry.d.ts.map +1 -0
  167. package/dist/tools/registry.js +65 -0
  168. package/dist/tools/registry.js.map +1 -0
  169. package/dist/tools/types.d.ts +24 -0
  170. package/dist/tools/types.d.ts.map +1 -0
  171. package/dist/tools/types.js +2 -0
  172. package/dist/tools/types.js.map +1 -0
  173. package/package.json +7 -6
  174. package/studio-plugin/MCPPlugin.rbxmx +3 -238
  175. package/studio-plugin/plugin.luau +2041 -365
package/dist/index.js CHANGED
@@ -2,17 +2,16 @@
2
2
  /**
3
3
  * Roblox Studio MCP Server
4
4
  *
5
- * This server provides Model Context Protocol (MCP) tools for interacting with Roblox Studio.
6
- * It allows AI assistants to access Studio data, scripts, and objects through a bridge plugin.
5
+ * This server provides Model Context Protocol (MCP) tools for interacting
6
+ * with Roblox Studio. It allows AI assistants to access Studio data,
7
+ * scripts, and objects through a bridge plugin.
7
8
  *
8
- * Usage:
9
- * npx robloxstudio-mcp
9
+ * Tools are defined declaratively in `src/tools/defs/*.ts` and aggregated
10
+ * by `src/tools/registry.ts`. To add a new tool, edit one of those files —
11
+ * this bootstrap doesn't need to change.
10
12
  *
11
- * Or add to your MCP configuration:
12
- * "robloxstudio": {
13
- * "command": "npx",
14
- * "args": ["-y", "robloxstudio-mcp"]
15
- * }
13
+ * Usage:
14
+ * npx rbxstudio-mcp
16
15
  */
17
16
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
17
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -20,6 +19,8 @@ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } f
20
19
  import { createHttpServer } from './http-server.js';
21
20
  import { RobloxStudioTools } from './tools/index.js';
22
21
  import { BridgeService } from './bridge-service.js';
22
+ import { allTools, toolsByName, applyNudge } from './tools/registry.js';
23
+ import { SERVER_INSTRUCTIONS } from './instructions.js';
23
24
  class RobloxStudioMCPServer {
24
25
  server;
25
26
  tools;
@@ -32,1039 +33,35 @@ class RobloxStudioMCPServer {
32
33
  capabilities: {
33
34
  tools: {},
34
35
  },
36
+ // Always-on operating manual (mental model + cross-tool workflow);
37
+ // returned in the initialize response and usually folded into the
38
+ // client's system prompt. See src/instructions.ts.
39
+ instructions: SERVER_INSTRUCTIONS,
35
40
  });
36
41
  this.bridge = new BridgeService();
37
42
  this.tools = new RobloxStudioTools(this.bridge);
38
43
  this.setupToolHandlers();
39
44
  }
40
45
  setupToolHandlers() {
41
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
42
- return {
43
- tools: [
44
- // Studio Context Tools
45
- {
46
- name: 'get_place_info',
47
- description: 'Get place ID, name, and game settings',
48
- inputSchema: {
49
- type: 'object',
50
- properties: {}
51
- }
52
- },
53
- {
54
- name: 'get_services',
55
- description: 'Get available Roblox services and their children',
56
- inputSchema: {
57
- type: 'object',
58
- properties: {
59
- serviceName: {
60
- type: 'string',
61
- description: 'Optional specific service name to query'
62
- }
63
- }
64
- }
65
- },
66
- // Property & Instance Tools
67
- {
68
- name: 'get_instance_properties',
69
- description: 'Get all properties of a specific Roblox instance in Studio',
70
- inputSchema: {
71
- type: 'object',
72
- properties: {
73
- instancePath: {
74
- type: 'string',
75
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part", "game.ServerScriptService.MainScript", "game.ReplicatedStorage.ModuleScript")'
76
- }
77
- },
78
- required: ['instancePath']
79
- }
80
- },
81
- {
82
- name: 'get_class_info',
83
- description: 'Get available properties/methods for Roblox classes',
84
- inputSchema: {
85
- type: 'object',
86
- properties: {
87
- className: {
88
- type: 'string',
89
- description: 'Roblox class name'
90
- }
91
- },
92
- required: ['className']
93
- }
94
- },
95
- // Project Tools
96
- {
97
- name: 'get_project_structure',
98
- description: 'Get complete game hierarchy. IMPORTANT: Use maxDepth parameter (default: 3) to explore deeper levels of the hierarchy. Set higher values like 5-10 for comprehensive exploration',
99
- inputSchema: {
100
- type: 'object',
101
- properties: {
102
- path: {
103
- type: 'string',
104
- description: 'Optional path to start from (defaults to workspace root)',
105
- default: ''
106
- },
107
- maxDepth: {
108
- type: 'number',
109
- description: 'Maximum depth to traverse (default: 3). RECOMMENDED: Use 5-10 for thorough exploration. Higher values provide more complete structure',
110
- default: 3
111
- },
112
- scriptsOnly: {
113
- type: 'boolean',
114
- description: 'Show only scripts and script containers',
115
- default: false
116
- }
117
- }
118
- }
119
- },
120
- // Property Modification Tools
121
- {
122
- name: 'set_property',
123
- description: 'Set a property on any Roblox instance',
124
- inputSchema: {
125
- type: 'object',
126
- properties: {
127
- instancePath: {
128
- type: 'string',
129
- description: 'Path to the instance (e.g., "game.Workspace.Part")'
130
- },
131
- propertyName: {
132
- type: 'string',
133
- description: 'Name of the property to set'
134
- },
135
- propertyValue: {
136
- description: 'Value to set the property to (any type)'
137
- }
138
- },
139
- required: ['instancePath', 'propertyName', 'propertyValue']
140
- }
141
- },
142
- {
143
- name: 'mass_set_property',
144
- description: 'Set the same property on multiple instances at once',
145
- inputSchema: {
146
- type: 'object',
147
- properties: {
148
- paths: {
149
- type: 'array',
150
- items: { type: 'string' },
151
- description: 'Array of instance paths to modify'
152
- },
153
- propertyName: {
154
- type: 'string',
155
- description: 'Name of the property to set'
156
- },
157
- propertyValue: {
158
- description: 'Value to set the property to (any type)'
159
- }
160
- },
161
- required: ['paths', 'propertyName', 'propertyValue']
162
- }
163
- },
164
- {
165
- name: 'mass_get_property',
166
- description: 'Get the same property from multiple instances at once',
167
- inputSchema: {
168
- type: 'object',
169
- properties: {
170
- paths: {
171
- type: 'array',
172
- items: { type: 'string' },
173
- description: 'Array of instance paths to read from'
174
- },
175
- propertyName: {
176
- type: 'string',
177
- description: 'Name of the property to get'
178
- }
179
- },
180
- required: ['paths', 'propertyName']
181
- }
182
- },
183
- // Object Creation/Deletion Tools
184
- {
185
- name: 'create_object',
186
- description: 'Create a new Roblox object instance (basic, without properties)',
187
- inputSchema: {
188
- type: 'object',
189
- properties: {
190
- className: {
191
- type: 'string',
192
- description: 'Roblox class name (e.g., "Part", "Script", "Folder")'
193
- },
194
- parent: {
195
- type: 'string',
196
- description: 'Path to the parent instance (e.g., "game.Workspace")'
197
- },
198
- name: {
199
- type: 'string',
200
- description: 'Optional name for the new object'
201
- }
202
- },
203
- required: ['className', 'parent']
204
- }
205
- },
206
- {
207
- name: 'delete_object',
208
- description: 'Delete a Roblox object instance',
209
- inputSchema: {
210
- type: 'object',
211
- properties: {
212
- instancePath: {
213
- type: 'string',
214
- description: 'Path to the instance to delete'
215
- }
216
- },
217
- required: ['instancePath']
218
- }
219
- },
220
- // Script Management Tools (for Roblox Studio scripts - NOT local files)
221
- {
222
- name: 'get_script_source',
223
- description: 'Get the source code of a Roblox script (LocalScript, Script, or ModuleScript). Returns both "source" (raw code) and "numberedSource" (with line numbers prefixed like "1: code"). Use numberedSource to accurately identify line numbers for editing. For large scripts (>1500 lines), use startLine/endLine to read specific sections.',
224
- inputSchema: {
225
- type: 'object',
226
- properties: {
227
- instancePath: {
228
- type: 'string',
229
- description: 'Roblox instance path to the script using dot notation (e.g., "game.ServerScriptService.MainScript", "game.StarterPlayer.StarterPlayerScripts.LocalScript")'
230
- },
231
- startLine: {
232
- type: 'number',
233
- description: 'Optional: Start line number (1-indexed). Use for reading specific sections of large scripts.'
234
- },
235
- endLine: {
236
- type: 'number',
237
- description: 'Optional: End line number (inclusive). Use for reading specific sections of large scripts.'
238
- }
239
- },
240
- required: ['instancePath']
241
- }
242
- },
243
- {
244
- name: 'set_script_source',
245
- description: 'Replace the entire source code of a Roblox script. Uses ScriptEditorService:UpdateSourceAsync (works with open editors). For partial edits, prefer edit_script_lines, insert_script_lines, or delete_script_lines.',
246
- inputSchema: {
247
- type: 'object',
248
- properties: {
249
- instancePath: {
250
- type: 'string',
251
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
252
- },
253
- source: {
254
- type: 'string',
255
- description: 'New source code for the script'
256
- }
257
- },
258
- required: ['instancePath', 'source']
259
- }
260
- },
261
- // Partial Script Editing Tools - use "numberedSource" from get_script_source to identify correct line numbers
262
- {
263
- name: 'edit_script_lines',
264
- description: 'Replace specific lines in a Roblox script without rewriting the entire source. IMPORTANT: Use the "numberedSource" field from get_script_source to identify the correct line numbers. Lines are 1-indexed and ranges are inclusive.',
265
- inputSchema: {
266
- type: 'object',
267
- properties: {
268
- instancePath: {
269
- type: 'string',
270
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
271
- },
272
- startLine: {
273
- type: 'number',
274
- description: 'First line to replace (1-indexed). Get this from the "numberedSource" field.'
275
- },
276
- endLine: {
277
- type: 'number',
278
- description: 'Last line to replace (inclusive). Get this from the "numberedSource" field.'
279
- },
280
- newContent: {
281
- type: 'string',
282
- description: 'New content to replace the specified lines (can be multiple lines separated by newlines)'
283
- }
284
- },
285
- required: ['instancePath', 'startLine', 'endLine', 'newContent']
286
- }
287
- },
288
- {
289
- name: 'insert_script_lines',
290
- description: 'Insert new lines into a Roblox script at a specific position. IMPORTANT: Use the "numberedSource" field from get_script_source to identify the correct line numbers.',
291
- inputSchema: {
292
- type: 'object',
293
- properties: {
294
- instancePath: {
295
- type: 'string',
296
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
297
- },
298
- afterLine: {
299
- type: 'number',
300
- description: 'Insert after this line number (0 = insert at very beginning, 1 = after first line). Get line numbers from "numberedSource".',
301
- default: 0
302
- },
303
- newContent: {
304
- type: 'string',
305
- description: 'Content to insert (can be multiple lines separated by newlines)'
306
- }
307
- },
308
- required: ['instancePath', 'newContent']
309
- }
310
- },
311
- {
312
- name: 'delete_script_lines',
313
- description: 'Delete specific lines from a Roblox script. IMPORTANT: Use the "numberedSource" field from get_script_source to identify the correct line numbers.',
314
- inputSchema: {
315
- type: 'object',
316
- properties: {
317
- instancePath: {
318
- type: 'string',
319
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
320
- },
321
- startLine: {
322
- type: 'number',
323
- description: 'First line to delete (1-indexed). Get this from the "numberedSource" field.'
324
- },
325
- endLine: {
326
- type: 'number',
327
- description: 'Last line to delete (inclusive). Get this from the "numberedSource" field.'
328
- }
329
- },
330
- required: ['instancePath', 'startLine', 'endLine']
331
- }
332
- },
333
- // ============================================
334
- // CLAUDE CODE-STYLE SCRIPT EDITING TOOLS
335
- // ============================================
336
- {
337
- name: 'edit_script',
338
- description: 'RECOMMENDED: String-based script editing like Claude Code\'s Edit tool. Find exact text and replace it - no line numbers needed! This is the safest and most reliable way to edit scripts. The edit will FAIL safely if old_string is not found or appears multiple times (unless replace_all is true). Always validates syntax after editing.',
339
- inputSchema: {
340
- type: 'object',
341
- properties: {
342
- instancePath: {
343
- type: 'string',
344
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
345
- },
346
- old_string: {
347
- type: 'string',
348
- description: 'The exact text to find and replace (must match exactly, including whitespace/indentation)'
349
- },
350
- new_string: {
351
- type: 'string',
352
- description: 'The text to replace it with (must be different from old_string)'
353
- },
354
- replace_all: {
355
- type: 'boolean',
356
- description: 'If true, replace ALL occurrences of old_string. If false (default), fails if old_string appears more than once.',
357
- default: false
358
- },
359
- validate_after: {
360
- type: 'boolean',
361
- description: 'If true (default), validates the script syntax after editing and reverts if invalid.',
362
- default: true
363
- }
364
- },
365
- required: ['instancePath', 'old_string', 'new_string']
366
- }
367
- },
368
- {
369
- name: 'search_script',
370
- description: 'Search for patterns within a script source code (like grep). Returns matching lines with line numbers and optional context.',
371
- inputSchema: {
372
- type: 'object',
373
- properties: {
374
- instancePath: {
375
- type: 'string',
376
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
377
- },
378
- pattern: {
379
- type: 'string',
380
- description: 'Search pattern (literal string or regex if use_regex is true)'
381
- },
382
- use_regex: {
383
- type: 'boolean',
384
- description: 'If true, treat pattern as a Lua regex pattern. Default false (literal match).',
385
- default: false
386
- },
387
- context_lines: {
388
- type: 'number',
389
- description: 'Number of lines to show before and after each match (like grep -C). Default 0.',
390
- default: 0
391
- }
392
- },
393
- required: ['instancePath', 'pattern']
394
- }
395
- },
396
- {
397
- name: 'get_script_function',
398
- description: 'Extract a specific function from a script by name. Returns the function source code with start/end line numbers. Perfect for editing just one function without affecting the rest of the script.',
399
- inputSchema: {
400
- type: 'object',
401
- properties: {
402
- instancePath: {
403
- type: 'string',
404
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
405
- },
406
- function_name: {
407
- type: 'string',
408
- description: 'Name of the function to extract (e.g., "onPlayerJoin", "handleDamage")'
409
- }
410
- },
411
- required: ['instancePath', 'function_name']
412
- }
413
- },
414
- {
415
- name: 'find_and_replace_in_scripts',
416
- description: 'Find and replace text across multiple scripts at once. Like edit_script but for batch operations. Validates all scripts after editing.',
417
- inputSchema: {
418
- type: 'object',
419
- properties: {
420
- paths: {
421
- type: 'array',
422
- items: { type: 'string' },
423
- description: 'Array of script paths to search and replace in'
424
- },
425
- old_string: {
426
- type: 'string',
427
- description: 'The exact text to find and replace'
428
- },
429
- new_string: {
430
- type: 'string',
431
- description: 'The text to replace it with'
432
- },
433
- validate_after: {
434
- type: 'boolean',
435
- description: 'If true (default), validates syntax after each edit.',
436
- default: true
437
- }
438
- },
439
- required: ['paths', 'old_string', 'new_string']
440
- }
441
- },
442
- // Attribute Tools (for Roblox instance attributes)
443
- {
444
- name: 'get_attribute',
445
- description: 'Get a single attribute value from a Roblox instance',
446
- inputSchema: {
447
- type: 'object',
448
- properties: {
449
- instancePath: {
450
- type: 'string',
451
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part", "game.ServerStorage.DataStore")'
452
- },
453
- attributeName: {
454
- type: 'string',
455
- description: 'Name of the attribute to get'
456
- }
457
- },
458
- required: ['instancePath', 'attributeName']
459
- }
460
- },
461
- {
462
- name: 'set_attribute',
463
- description: 'Set an attribute value on a Roblox instance. Supports string, number, boolean, Vector3, Color3, UDim2, and BrickColor.',
464
- inputSchema: {
465
- type: 'object',
466
- properties: {
467
- instancePath: {
468
- type: 'string',
469
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
470
- },
471
- attributeName: {
472
- type: 'string',
473
- description: 'Name of the attribute to set'
474
- },
475
- attributeValue: {
476
- description: 'Value to set. For Vector3: {X, Y, Z}, Color3: {R, G, B}, UDim2: {X: {Scale, Offset}, Y: {Scale, Offset}}'
477
- },
478
- valueType: {
479
- type: 'string',
480
- description: 'Optional type hint: "Vector3", "Color3", "UDim2", "BrickColor"'
481
- }
482
- },
483
- required: ['instancePath', 'attributeName', 'attributeValue']
484
- }
485
- },
486
- {
487
- name: 'get_attributes',
488
- description: 'Get all attributes on a Roblox instance',
489
- inputSchema: {
490
- type: 'object',
491
- properties: {
492
- instancePath: {
493
- type: 'string',
494
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
495
- }
496
- },
497
- required: ['instancePath']
498
- }
499
- },
500
- {
501
- name: 'delete_attribute',
502
- description: 'Delete an attribute from a Roblox instance',
503
- inputSchema: {
504
- type: 'object',
505
- properties: {
506
- instancePath: {
507
- type: 'string',
508
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
509
- },
510
- attributeName: {
511
- type: 'string',
512
- description: 'Name of the attribute to delete'
513
- }
514
- },
515
- required: ['instancePath', 'attributeName']
516
- }
517
- },
518
- // Tag Tools (CollectionService) - for Roblox instance tags
519
- {
520
- name: 'get_tags',
521
- description: 'Get all CollectionService tags on a Roblox instance',
522
- inputSchema: {
523
- type: 'object',
524
- properties: {
525
- instancePath: {
526
- type: 'string',
527
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
528
- }
529
- },
530
- required: ['instancePath']
531
- }
532
- },
533
- {
534
- name: 'add_tag',
535
- description: 'Add a CollectionService tag to a Roblox instance',
536
- inputSchema: {
537
- type: 'object',
538
- properties: {
539
- instancePath: {
540
- type: 'string',
541
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
542
- },
543
- tagName: {
544
- type: 'string',
545
- description: 'Name of the tag to add'
546
- }
547
- },
548
- required: ['instancePath', 'tagName']
549
- }
550
- },
551
- {
552
- name: 'remove_tag',
553
- description: 'Remove a CollectionService tag from a Roblox instance',
554
- inputSchema: {
555
- type: 'object',
556
- properties: {
557
- instancePath: {
558
- type: 'string',
559
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
560
- },
561
- tagName: {
562
- type: 'string',
563
- description: 'Name of the tag to remove'
564
- }
565
- },
566
- required: ['instancePath', 'tagName']
567
- }
568
- },
569
- {
570
- name: 'get_tagged',
571
- description: 'Get all instances with a specific tag',
572
- inputSchema: {
573
- type: 'object',
574
- properties: {
575
- tagName: {
576
- type: 'string',
577
- description: 'Name of the tag to search for'
578
- }
579
- },
580
- required: ['tagName']
581
- }
582
- },
583
- {
584
- name: 'get_selection',
585
- description: 'Get all currently selected objects',
586
- inputSchema: {
587
- type: 'object',
588
- properties: {}
589
- }
590
- },
591
- // ============================================
592
- // OUTPUT CAPTURE TOOL
593
- // ============================================
594
- {
595
- name: 'get_output',
596
- description: 'Read the Output window content from Roblox Studio. Captures print(), warn(), and error() messages. Use after play_solo to debug scripts.',
597
- inputSchema: {
598
- type: 'object',
599
- properties: {
600
- limit: {
601
- type: 'number',
602
- description: 'Maximum number of messages to return (default: 100)',
603
- default: 100
604
- },
605
- since: {
606
- type: 'number',
607
- description: 'Only return messages after this Unix timestamp'
608
- },
609
- messageTypes: {
610
- type: 'array',
611
- items: { type: 'string' },
612
- description: 'Filter by message type: MessageOutput, MessageInfo, MessageWarning, MessageError'
613
- },
614
- clear: {
615
- type: 'boolean',
616
- description: 'Clear the output buffer after reading (default: false)',
617
- default: false
618
- }
619
- }
620
- }
621
- },
622
- // ============================================
623
- // INSTANCE MANIPULATION TOOLS
624
- // ============================================
625
- {
626
- name: 'clone_instance',
627
- description: 'Clone (copy) a Roblox instance to a new parent location. Creates a deep copy including all children and properties.',
628
- inputSchema: {
629
- type: 'object',
630
- properties: {
631
- sourcePath: {
632
- type: 'string',
633
- description: 'Path to the instance to clone (e.g., "game.Workspace.walkietalkie")'
634
- },
635
- targetParent: {
636
- type: 'string',
637
- description: 'Path to the new parent (e.g., "game.ReplicatedStorage")'
638
- },
639
- newName: {
640
- type: 'string',
641
- description: 'Optional new name for the cloned instance'
642
- }
643
- },
644
- required: ['sourcePath', 'targetParent']
645
- }
646
- },
647
- {
648
- name: 'move_instance',
649
- description: 'Move a Roblox instance to a new parent location. Changes the Parent property.',
650
- inputSchema: {
651
- type: 'object',
652
- properties: {
653
- instancePath: {
654
- type: 'string',
655
- description: 'Path to the instance to move (e.g., "game.Workspace.Tool")'
656
- },
657
- newParent: {
658
- type: 'string',
659
- description: 'Path to the new parent (e.g., "game.StarterPack")'
660
- }
661
- },
662
- required: ['instancePath', 'newParent']
663
- }
664
- },
665
- // ============================================
666
- // SCRIPT VALIDATION TOOL
667
- // ============================================
668
- {
669
- name: 'validate_script',
670
- description: 'Validate Lua/Luau script syntax without running it. Returns syntax errors and warnings for deprecated patterns (wait, spawn, delay). Can validate either an existing script or raw source code.',
671
- inputSchema: {
672
- type: 'object',
673
- properties: {
674
- instancePath: {
675
- type: 'string',
676
- description: 'Path to the script to validate (e.g., "game.ServerScriptService.MainScript")'
677
- },
678
- source: {
679
- type: 'string',
680
- description: 'Raw Lua source code to validate (alternative to instancePath)'
681
- }
682
- }
683
- }
684
- },
685
- // ============================================
686
- // UNDO/REDO TOOLS
687
- // ============================================
688
- {
689
- name: 'undo',
690
- description: 'Undo the last change made in Roblox Studio. All MCP mutations are automatically recorded for undo support. Use this to revert mistakes.',
691
- inputSchema: {
692
- type: 'object',
693
- properties: {}
694
- }
695
- },
696
- {
697
- name: 'redo',
698
- description: 'Redo a previously undone change in Roblox Studio.',
699
- inputSchema: {
700
- type: 'object',
701
- properties: {}
702
- }
703
- },
704
- // ============================================
705
- // ASSET INSERTION TOOL (Creator Store)
706
- // ============================================
707
- {
708
- name: 'insert_asset',
709
- description: 'Download and insert a Creator Store asset (model, package, etc.) into Roblox Studio for reference. Uses game:GetObjects() which works with any free/public asset. Perfect for loading reference code, example implementations, or asset libraries that the AI can then read and analyze using get_instance_children and get_script_source.',
710
- inputSchema: {
711
- type: 'object',
712
- properties: {
713
- assetId: {
714
- type: 'number',
715
- description: 'The Creator Store asset ID (the number from the asset URL, e.g., 104116977416770)'
716
- },
717
- folderName: {
718
- type: 'string',
719
- description: 'Name of the folder to create/use for storing assets (default: "AIReferences")',
720
- default: 'AIReferences'
721
- },
722
- targetParent: {
723
- type: 'string',
724
- description: 'Parent path where the folder should be created (default: "game.Workspace"). Use "game.ReplicatedStorage" or "game.ServerStorage" to keep assets out of the visible workspace.',
725
- default: 'game.Workspace'
726
- }
727
- },
728
- required: ['assetId']
729
- }
730
- },
731
- // ============================================
732
- // PLAYTEST CONTROL TOOLS
733
- // ============================================
734
- {
735
- name: 'play_solo',
736
- description: 'Start a play test (Play Solo) in Roblox Studio via StudioTestService:ExecutePlayModeAsync. Automatically injects an in-test companion script so stop_play and get_playtest_output work. If a previous test is still tracked, stops it first. Returns a sessionId that ties together start, stop, and output reads.',
737
- inputSchema: {
738
- type: 'object',
739
- properties: {}
740
- }
741
- },
742
- {
743
- name: 'stop_play',
744
- description: 'Stop the current play test cleanly via StudioTestService:EndTest (called from inside the test by an injected companion script). Restores pre-play state — unlike RunService:Stop, this is the proper way to end a Play Solo session. Idempotent: returns successfully if no test is running.',
745
- inputSchema: {
746
- type: 'object',
747
- properties: {}
748
- }
749
- },
750
- {
751
- name: 'get_playtest_output',
752
- description: 'Read script output (print/warn/error) captured DURING a play test session. Streamed live from the test\'s Server DataModel by the injected companion. Survives after the test ends so you can debug post-hoc. For non-playtest output (Edit-mode plugin output, build messages), use get_output instead. Use the sinceSeq cursor returned in nextSinceSeq to tail incrementally.',
753
- inputSchema: {
754
- type: 'object',
755
- properties: {
756
- sinceSeq: {
757
- type: 'number',
758
- description: 'Only return entries with seq > this value. Pass back nextSinceSeq from a prior call to avoid re-reading.',
759
- },
760
- limit: {
761
- type: 'number',
762
- description: 'Max entries to return (default 500, max 5000).',
763
- default: 500,
764
- },
765
- messageTypes: {
766
- type: 'array',
767
- items: { type: 'string' },
768
- description: 'Filter by MessageType: MessageOutput, MessageInfo, MessageWarning, MessageError.',
769
- },
770
- },
771
- },
772
- },
773
- // ============================================
774
- // SCREENSHOT TOOL
775
- // ============================================
776
- {
777
- name: 'capture_screenshot',
778
- description: 'Capture a screenshot of the current Roblox Studio viewport. Returns the image as base64-encoded RGBA pixel data. Use this to "see" what you\'ve built - GUIs, 3D objects, scene layout, etc. The screenshot captures exactly what\'s visible in the Studio viewport.',
779
- inputSchema: {
780
- type: 'object',
781
- properties: {
782
- maxWidth: {
783
- type: 'number',
784
- description: 'Maximum width of the returned image (default: 768). Smaller = faster + less data.',
785
- default: 768
786
- },
787
- maxHeight: {
788
- type: 'number',
789
- description: 'Maximum height of the returned image (default: 768). Smaller = faster + less data.',
790
- default: 768
791
- }
792
- }
793
- }
794
- },
795
- // ============================================
796
- // VIEWPORTFRAME RENDERING (Visual feedback)
797
- // ============================================
798
- {
799
- name: 'render_object_view',
800
- description: `Render an object as an image from any angle using ViewportFrame. This is the PRIMARY tool for visual feedback - use it whenever you need to "see" what you've created or verify visual appearance.
801
-
802
- Works in ANY Studio state (Edit/Play/Run) - no CaptureService limitations!
803
- Instant rendering with full control over camera, lighting, and background.
804
-
805
- Use cases:
806
- - Verify visual appearance of created objects
807
- - Generate thumbnails/previews
808
- - Debug positioning and orientation
809
- - Iterate on visual designs
810
- - Show users what their objects look like
811
-
812
- Available camera angles: front, back, left, right, top, bottom, iso (isometric), iso_front, iso_back, low_angle, high_angle
813
- Or provide custom angles with pitch/yaw/roll in degrees.
814
-
815
- Lighting presets: bright (3-point lighting), studio (flat/even), dark (dramatic), default (ambient only)`,
816
- inputSchema: {
817
- type: 'object',
818
- properties: {
819
- instancePath: {
820
- type: 'string',
821
- description: 'Path to the object to render (e.g., "game.Workspace.Model1")',
822
- },
823
- angle: {
824
- description: 'Camera angle - use preset string or custom object with pitch/yaw/roll/distance',
825
- oneOf: [
826
- {
827
- type: 'string',
828
- enum: ['front', 'back', 'left', 'right', 'top', 'bottom', 'iso', 'iso_front', 'iso_back', 'low_angle', 'high_angle'],
829
- },
830
- {
831
- type: 'object',
832
- properties: {
833
- pitch: { type: 'number', description: 'Pitch angle in degrees' },
834
- yaw: { type: 'number', description: 'Yaw angle in degrees' },
835
- roll: { type: 'number', description: 'Roll angle in degrees' },
836
- distance: { type: 'number', description: 'Camera distance from object' },
837
- },
838
- },
839
- ],
840
- },
841
- resolution: {
842
- type: 'object',
843
- properties: {
844
- width: { type: 'number', description: 'Image width (64-2048, default: 768)' },
845
- height: { type: 'number', description: 'Image height (64-2048, default: 768)' },
846
- },
847
- description: 'Render resolution',
848
- },
849
- lighting: {
850
- type: 'string',
851
- enum: ['default', 'bright', 'studio', 'dark', 'showcase', 'dramatic', 'flat'],
852
- description: 'Lighting preset to use (default: bright)',
853
- },
854
- background: {
855
- type: 'string',
856
- enum: ['transparent', 'grid', 'solid'],
857
- description: 'Background style (default: transparent)',
858
- },
859
- autoDistance: {
860
- type: 'boolean',
861
- description: 'Automatically calculate camera distance to fit object (default: true)',
862
- },
863
- },
864
- required: ['instancePath'],
865
- },
866
- },
867
- // ============================================
868
- // CAMERA CONTROL (Focus Studio camera)
869
- // ============================================
870
- {
871
- name: 'focus_camera',
872
- description: `Position the Studio camera to focus on an object (like pressing F in Studio).
873
- Automatically calculates distance to fit the object in view, works with any object size.
874
-
875
- Perfect combo with capture_screenshot:
876
- 1. focus_camera({instancePath: "...", angle: "front"})
877
- 2. capture_screenshot()
878
-
879
- Supported angles:
880
- - Standard views: front, back, left, right, top, bottom
881
- - Isometric: iso (default), iso_front, iso_back
882
- - Dramatic: low_angle, high_angle
883
- - Custom: {pitch: 30, yaw: 45, roll: 0}
884
-
885
- Auto-sizing:
886
- - Tiny objects (0.1 studs): Camera backs up to minimum 5 studs
887
- - Normal objects (10 studs): Camera positioned perfectly
888
- - Huge objects (1000 studs): Camera backs up far enough to see everything`,
889
- inputSchema: {
890
- type: 'object',
891
- properties: {
892
- instancePath: {
893
- type: 'string',
894
- description: 'Path to the object to focus on (e.g., "game.Workspace.Model1")',
895
- },
896
- angle: {
897
- description: 'Camera angle - preset string or custom {pitch, yaw, roll}',
898
- oneOf: [
899
- {
900
- type: 'string',
901
- enum: ['front', 'back', 'left', 'right', 'top', 'bottom', 'iso', 'iso_front', 'iso_back', 'low_angle', 'high_angle'],
902
- },
903
- {
904
- type: 'object',
905
- properties: {
906
- pitch: { type: 'number', description: 'Pitch angle in degrees' },
907
- yaw: { type: 'number', description: 'Yaw angle in degrees' },
908
- roll: { type: 'number', description: 'Roll angle in degrees' },
909
- },
910
- },
911
- ],
912
- },
913
- distance: {
914
- type: 'number',
915
- description: 'Manual camera distance (overrides auto-distance)',
916
- },
917
- autoDistance: {
918
- type: 'boolean',
919
- description: 'Automatically calculate distance to fit object (default: true)',
920
- },
921
- },
922
- required: ['instancePath'],
923
- },
924
- },
925
- // ============================================
926
- // EXECUTE LUA TOOL (Run arbitrary Lua code)
927
- // ============================================
928
- {
929
- name: 'execute_lua',
930
- description: 'Execute arbitrary Lua/Luau code in Roblox Studio. This is a powerful tool that runs code directly in the Studio plugin context with access to all services, Instance constructors, and the full Roblox API. Use this for complex operations that would require multiple tool calls, debugging, prototyping, or any task that\'s easier to express in code. Returns the result of the last expression.',
931
- inputSchema: {
932
- type: 'object',
933
- properties: {
934
- code: {
935
- type: 'string',
936
- description: 'The Lua/Luau code to execute. Has access to: game, workspace, all services (Players, ReplicatedStorage, etc.), Instance constructors (Vector3, CFrame, Color3, etc.), and helper function getInstanceByPath(path). Return a value to get it back in the response.'
937
- }
938
- },
939
- required: ['code']
940
- }
941
- }
942
- ]
943
- };
944
- });
46
+ // List tools — strip handlers, expose only MCP-visible fields.
47
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
48
+ tools: allTools.map(({ name, description, inputSchema }) => ({
49
+ name,
50
+ description,
51
+ inputSchema,
52
+ })),
53
+ }));
54
+ // Dispatch a single tool call via the registry.
945
55
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
946
56
  const { name, arguments: args } = request.params;
57
+ const tool = toolsByName[name];
58
+ if (!tool) {
59
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
60
+ }
947
61
  try {
948
- switch (name) {
949
- // Studio Context Tools
950
- case 'get_place_info':
951
- return await this.tools.getPlaceInfo();
952
- case 'get_services':
953
- return await this.tools.getServices(args?.serviceName);
954
- // Property & Instance Tools
955
- case 'get_instance_properties':
956
- return await this.tools.getInstanceProperties(args?.instancePath);
957
- case 'get_class_info':
958
- return await this.tools.getClassInfo(args?.className);
959
- // Project Tools
960
- case 'get_project_structure':
961
- return await this.tools.getProjectStructure(args?.path, args?.maxDepth, args?.scriptsOnly);
962
- // Property Modification Tools
963
- case 'set_property':
964
- return await this.tools.setProperty(args?.instancePath, args?.propertyName, args?.propertyValue);
965
- // Mass Property Tools
966
- case 'mass_set_property':
967
- return await this.tools.massSetProperty(args?.paths, args?.propertyName, args?.propertyValue);
968
- case 'mass_get_property':
969
- return await this.tools.massGetProperty(args?.paths, args?.propertyName);
970
- // Object Creation/Deletion Tools
971
- case 'create_object':
972
- return await this.tools.createObject(args?.className, args?.parent, args?.name);
973
- case 'delete_object':
974
- return await this.tools.deleteObject(args?.instancePath);
975
- // Script Management Tools
976
- case 'get_script_source':
977
- return await this.tools.getScriptSource(args?.instancePath, args?.startLine, args?.endLine);
978
- case 'set_script_source':
979
- return await this.tools.setScriptSource(args?.instancePath, args?.source);
980
- // Partial Script Editing Tools (line-based - legacy)
981
- case 'edit_script_lines':
982
- return await this.tools.editScriptLines(args?.instancePath, args?.startLine, args?.endLine, args?.newContent);
983
- case 'insert_script_lines':
984
- return await this.tools.insertScriptLines(args?.instancePath, args?.afterLine, args?.newContent);
985
- case 'delete_script_lines':
986
- return await this.tools.deleteScriptLines(args?.instancePath, args?.startLine, args?.endLine);
987
- // Claude Code-Style Script Editing Tools (RECOMMENDED)
988
- case 'edit_script':
989
- return await this.tools.editScript(args?.instancePath, args?.old_string, args?.new_string, args?.replace_all ?? false, args?.validate_after ?? true);
990
- case 'search_script':
991
- return await this.tools.searchScript(args?.instancePath, args?.pattern, args?.use_regex ?? false, args?.context_lines ?? 0);
992
- case 'get_script_function':
993
- return await this.tools.getScriptFunction(args?.instancePath, args?.function_name);
994
- case 'find_and_replace_in_scripts':
995
- return await this.tools.findAndReplaceInScripts(args?.paths, args?.old_string, args?.new_string, args?.validate_after ?? true);
996
- // Attribute Tools
997
- case 'get_attribute':
998
- return await this.tools.getAttribute(args?.instancePath, args?.attributeName);
999
- case 'set_attribute':
1000
- return await this.tools.setAttribute(args?.instancePath, args?.attributeName, args?.attributeValue, args?.valueType);
1001
- case 'get_attributes':
1002
- return await this.tools.getAttributes(args?.instancePath);
1003
- case 'delete_attribute':
1004
- return await this.tools.deleteAttribute(args?.instancePath, args?.attributeName);
1005
- // Tag Tools (CollectionService)
1006
- case 'get_tags':
1007
- return await this.tools.getTags(args?.instancePath);
1008
- case 'add_tag':
1009
- return await this.tools.addTag(args?.instancePath, args?.tagName);
1010
- case 'remove_tag':
1011
- return await this.tools.removeTag(args?.instancePath, args?.tagName);
1012
- case 'get_tagged':
1013
- return await this.tools.getTagged(args?.tagName);
1014
- // Selection Tools
1015
- case 'get_selection':
1016
- return await this.tools.getSelection();
1017
- // Output Capture Tool
1018
- case 'get_output':
1019
- return await this.tools.getOutput(args?.limit, args?.since, args?.messageTypes, args?.clear);
1020
- // Instance Manipulation Tools
1021
- case 'clone_instance':
1022
- return await this.tools.cloneInstance(args?.sourcePath, args?.targetParent, args?.newName);
1023
- case 'move_instance':
1024
- return await this.tools.moveInstance(args?.instancePath, args?.newParent);
1025
- // Script Validation Tool
1026
- case 'validate_script':
1027
- return await this.tools.validateScript(args?.instancePath, args?.source);
1028
- // Undo/Redo Tools
1029
- case 'undo':
1030
- return await this.tools.undo();
1031
- case 'redo':
1032
- return await this.tools.redo();
1033
- // Asset Insertion Tool
1034
- case 'insert_asset':
1035
- return await this.tools.insertAsset(args?.assetId, args?.folderName, args?.targetParent);
1036
- // Playtest Control Tools
1037
- case 'play_solo':
1038
- return await this.tools.playSolo();
1039
- case 'stop_play':
1040
- return await this.tools.stopPlay();
1041
- case 'get_playtest_output':
1042
- return await this.tools.getPlaytestOutput(args?.sinceSeq, args?.limit, args?.messageTypes);
1043
- // Screenshot Tool
1044
- case 'capture_screenshot':
1045
- return await this.tools.captureScreenshot(args?.maxWidth, args?.maxHeight);
1046
- // ViewportFrame Rendering
1047
- case 'render_object_view':
1048
- return await this.tools.renderObjectView(args?.instancePath, {
1049
- angle: args?.angle,
1050
- resolution: args?.resolution,
1051
- lighting: args?.lighting,
1052
- background: args?.background,
1053
- autoDistance: args?.autoDistance,
1054
- });
1055
- // Camera Control
1056
- case 'focus_camera':
1057
- return await this.tools.focusCamera(args?.instancePath, {
1058
- angle: args?.angle,
1059
- distance: args?.distance,
1060
- autoDistance: args?.autoDistance,
1061
- });
1062
- // Execute Lua Tool
1063
- case 'execute_lua':
1064
- return await this.tools.executeLua(args?.code);
1065
- default:
1066
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1067
- }
62
+ const result = await tool.handler(args ?? {}, { tools: this.tools });
63
+ // Append the tool's just-in-time steering (no-op if it has none).
64
+ return applyNudge(result, tool.nudge);
1068
65
  }
1069
66
  catch (error) {
1070
67
  throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -1072,7 +69,9 @@ Auto-sizing:
1072
69
  });
1073
70
  }
1074
71
  async run() {
1075
- const port = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 3002;
72
+ const port = process.env.ROBLOX_STUDIO_PORT
73
+ ? parseInt(process.env.ROBLOX_STUDIO_PORT)
74
+ : 3002;
1076
75
  const host = process.env.ROBLOX_STUDIO_HOST || '0.0.0.0';
1077
76
  const httpServer = createHttpServer(this.tools, this.bridge);
1078
77
  await new Promise((resolve) => {
@@ -1086,11 +85,12 @@ Auto-sizing:
1086
85
  console.error('Roblox Studio MCP server running on stdio');
1087
86
  httpServer.setMCPServerActive(true);
1088
87
  console.error('MCP server marked as active');
1089
- console.error('Waiting for Studio plugin to connect...');
88
+ console.error(`Registered ${allTools.length} tools, waiting for Studio plugin...`);
1090
89
  setInterval(() => {
1091
90
  const pluginConnected = httpServer.isPluginConnected();
1092
91
  const mcpActive = httpServer.isMCPServerActive();
1093
92
  if (pluginConnected && mcpActive) {
93
+ // both up; quiet
1094
94
  }
1095
95
  else if (pluginConnected && !mcpActive) {
1096
96
  console.error('Studio plugin connected, but MCP server inactive');