robloxstudio-mcp 1.0.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/CLAUDE.md +63 -0
- package/README.md +104 -0
- package/dist/bridge-service.d.ts +16 -0
- package/dist/bridge-service.d.ts.map +1 -0
- package/dist/bridge-service.js +70 -0
- package/dist/bridge-service.js.map +1 -0
- package/dist/http-server.d.ts +4 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +170 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +323 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/index.d.ts +96 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +202 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/studio-client.d.ts +7 -0
- package/dist/tools/studio-client.d.ts.map +1 -0
- package/dist/tools/studio-client.js +19 -0
- package/dist/tools/studio-client.js.map +1 -0
- package/package.json +54 -0
- package/studio-plugin/INSTALLATION.md +105 -0
- package/studio-plugin/plugin.json +10 -0
- package/studio-plugin/plugin.lua +683 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
-- Roblox Studio MCP Plugin
|
|
2
|
+
-- This plugin communicates with the MCP server to provide Studio data access
|
|
3
|
+
|
|
4
|
+
local HttpService = game:GetService("HttpService")
|
|
5
|
+
local StudioService = game:GetService("StudioService")
|
|
6
|
+
local Selection = game:GetService("Selection")
|
|
7
|
+
local RunService = game:GetService("RunService")
|
|
8
|
+
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
9
|
+
|
|
10
|
+
-- Create plugin toolbar and button
|
|
11
|
+
local toolbar = plugin:CreateToolbar("MCP Integration")
|
|
12
|
+
local button = toolbar:CreateButton(
|
|
13
|
+
"MCP Server",
|
|
14
|
+
"Connect to MCP Server for AI Integration",
|
|
15
|
+
"rbxasset://textures/ui/GuiImagePlaceholder.png"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
-- Plugin state
|
|
19
|
+
local pluginState = {
|
|
20
|
+
serverUrl = "http://localhost:3002",
|
|
21
|
+
mcpServerUrl = "http://localhost:3001",
|
|
22
|
+
isActive = false,
|
|
23
|
+
pollInterval = 0.5, -- Poll every 500ms
|
|
24
|
+
lastPoll = 0,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
-- Create plugin GUI
|
|
28
|
+
local screenGui = plugin:CreateDockWidgetPluginGui(
|
|
29
|
+
"MCPServerInterface",
|
|
30
|
+
DockWidgetPluginGuiInfo.new(
|
|
31
|
+
Enum.InitialDockState.Float,
|
|
32
|
+
false, -- Widget will be initially disabled
|
|
33
|
+
false, -- Don't override the previous enabled state
|
|
34
|
+
300, -- Default width
|
|
35
|
+
200, -- Default height
|
|
36
|
+
280, -- Minimum width
|
|
37
|
+
180 -- Minimum height
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
screenGui.Title = "MCP Server Interface"
|
|
41
|
+
|
|
42
|
+
-- Main container frame
|
|
43
|
+
local mainFrame = Instance.new("Frame")
|
|
44
|
+
mainFrame.Size = UDim2.new(1, 0, 1, 0)
|
|
45
|
+
mainFrame.BackgroundColor3 = Color3.fromRGB(46, 46, 46)
|
|
46
|
+
mainFrame.BorderSizePixel = 0
|
|
47
|
+
mainFrame.Parent = screenGui
|
|
48
|
+
|
|
49
|
+
-- Header frame
|
|
50
|
+
local headerFrame = Instance.new("Frame")
|
|
51
|
+
headerFrame.Size = UDim2.new(1, 0, 0, 40)
|
|
52
|
+
headerFrame.Position = UDim2.new(0, 0, 0, 0)
|
|
53
|
+
headerFrame.BackgroundColor3 = Color3.fromRGB(37, 37, 37)
|
|
54
|
+
headerFrame.BorderSizePixel = 0
|
|
55
|
+
headerFrame.Parent = mainFrame
|
|
56
|
+
|
|
57
|
+
-- Title label
|
|
58
|
+
local titleLabel = Instance.new("TextLabel")
|
|
59
|
+
titleLabel.Size = UDim2.new(1, -10, 1, 0)
|
|
60
|
+
titleLabel.Position = UDim2.new(0, 10, 0, 0)
|
|
61
|
+
titleLabel.BackgroundTransparency = 1
|
|
62
|
+
titleLabel.Text = "MCP Server"
|
|
63
|
+
titleLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
64
|
+
titleLabel.TextScaled = false
|
|
65
|
+
titleLabel.TextSize = 16
|
|
66
|
+
titleLabel.Font = Enum.Font.SourceSansBold
|
|
67
|
+
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
68
|
+
titleLabel.Parent = headerFrame
|
|
69
|
+
|
|
70
|
+
-- Status indicator
|
|
71
|
+
local statusIndicator = Instance.new("Frame")
|
|
72
|
+
statusIndicator.Size = UDim2.new(0, 12, 0, 12)
|
|
73
|
+
statusIndicator.Position = UDim2.new(1, -20, 0.5, -6)
|
|
74
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(255, 85, 85) -- Red for disconnected
|
|
75
|
+
statusIndicator.BorderSizePixel = 0
|
|
76
|
+
statusIndicator.Parent = headerFrame
|
|
77
|
+
|
|
78
|
+
-- Round the status indicator
|
|
79
|
+
local corner = Instance.new("UICorner")
|
|
80
|
+
corner.CornerRadius = UDim.new(1, 0)
|
|
81
|
+
corner.Parent = statusIndicator
|
|
82
|
+
|
|
83
|
+
-- Content frame
|
|
84
|
+
local contentFrame = Instance.new("Frame")
|
|
85
|
+
contentFrame.Size = UDim2.new(1, -20, 1, -60)
|
|
86
|
+
contentFrame.Position = UDim2.new(0, 10, 0, 50)
|
|
87
|
+
contentFrame.BackgroundTransparency = 1
|
|
88
|
+
contentFrame.Parent = mainFrame
|
|
89
|
+
|
|
90
|
+
-- Server URL input
|
|
91
|
+
local urlLabel = Instance.new("TextLabel")
|
|
92
|
+
urlLabel.Size = UDim2.new(1, 0, 0, 20)
|
|
93
|
+
urlLabel.Position = UDim2.new(0, 0, 0, 0)
|
|
94
|
+
urlLabel.BackgroundTransparency = 1
|
|
95
|
+
urlLabel.Text = "Server URL:"
|
|
96
|
+
urlLabel.TextColor3 = Color3.fromRGB(200, 200, 200)
|
|
97
|
+
urlLabel.TextScaled = false
|
|
98
|
+
urlLabel.TextSize = 14
|
|
99
|
+
urlLabel.Font = Enum.Font.SourceSans
|
|
100
|
+
urlLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
101
|
+
urlLabel.Parent = contentFrame
|
|
102
|
+
|
|
103
|
+
local urlInput = Instance.new("TextBox")
|
|
104
|
+
urlInput.Size = UDim2.new(1, 0, 0, 25)
|
|
105
|
+
urlInput.Position = UDim2.new(0, 0, 0, 25)
|
|
106
|
+
urlInput.BackgroundColor3 = Color3.fromRGB(60, 60, 60)
|
|
107
|
+
urlInput.BorderSizePixel = 0
|
|
108
|
+
urlInput.Text = "http://localhost:3002"
|
|
109
|
+
urlInput.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
110
|
+
urlInput.TextScaled = false
|
|
111
|
+
urlInput.TextSize = 12
|
|
112
|
+
urlInput.Font = Enum.Font.SourceSans
|
|
113
|
+
urlInput.ClearTextOnFocus = false
|
|
114
|
+
urlInput.Parent = contentFrame
|
|
115
|
+
|
|
116
|
+
-- Round the input
|
|
117
|
+
local inputCorner = Instance.new("UICorner")
|
|
118
|
+
inputCorner.CornerRadius = UDim.new(0, 4)
|
|
119
|
+
inputCorner.Parent = urlInput
|
|
120
|
+
|
|
121
|
+
-- Padding for input
|
|
122
|
+
local inputPadding = Instance.new("UIPadding")
|
|
123
|
+
inputPadding.PaddingLeft = UDim.new(0, 8)
|
|
124
|
+
inputPadding.PaddingRight = UDim.new(0, 8)
|
|
125
|
+
inputPadding.Parent = urlInput
|
|
126
|
+
|
|
127
|
+
-- Status label
|
|
128
|
+
local statusLabel = Instance.new("TextLabel")
|
|
129
|
+
statusLabel.Size = UDim2.new(1, 0, 0, 20)
|
|
130
|
+
statusLabel.Position = UDim2.new(0, 0, 0, 60)
|
|
131
|
+
statusLabel.BackgroundTransparency = 1
|
|
132
|
+
statusLabel.Text = "Status: Disconnected"
|
|
133
|
+
statusLabel.TextColor3 = Color3.fromRGB(255, 85, 85)
|
|
134
|
+
statusLabel.TextScaled = false
|
|
135
|
+
statusLabel.TextSize = 14
|
|
136
|
+
statusLabel.Font = Enum.Font.SourceSans
|
|
137
|
+
statusLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
138
|
+
statusLabel.Parent = contentFrame
|
|
139
|
+
|
|
140
|
+
-- Connect/Disconnect button
|
|
141
|
+
local connectButton = Instance.new("TextButton")
|
|
142
|
+
connectButton.Size = UDim2.new(1, 0, 0, 35)
|
|
143
|
+
connectButton.Position = UDim2.new(0, 0, 0, 90)
|
|
144
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(0, 162, 255)
|
|
145
|
+
connectButton.BorderSizePixel = 0
|
|
146
|
+
connectButton.Text = "Connect"
|
|
147
|
+
connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
148
|
+
connectButton.TextScaled = false
|
|
149
|
+
connectButton.TextSize = 16
|
|
150
|
+
connectButton.Font = Enum.Font.SourceSansBold
|
|
151
|
+
connectButton.Parent = contentFrame
|
|
152
|
+
|
|
153
|
+
-- Round the button
|
|
154
|
+
local buttonCorner = Instance.new("UICorner")
|
|
155
|
+
buttonCorner.CornerRadius = UDim.new(0, 6)
|
|
156
|
+
buttonCorner.Parent = connectButton
|
|
157
|
+
|
|
158
|
+
-- Button hover effect
|
|
159
|
+
local buttonHover = false
|
|
160
|
+
connectButton.MouseEnter:Connect(function()
|
|
161
|
+
buttonHover = true
|
|
162
|
+
if not pluginState.isActive then
|
|
163
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(30, 180, 255)
|
|
164
|
+
else
|
|
165
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(255, 100, 100)
|
|
166
|
+
end
|
|
167
|
+
end)
|
|
168
|
+
|
|
169
|
+
connectButton.MouseLeave:Connect(function()
|
|
170
|
+
buttonHover = false
|
|
171
|
+
if not pluginState.isActive then
|
|
172
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(0, 162, 255)
|
|
173
|
+
else
|
|
174
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(255, 85, 85)
|
|
175
|
+
end
|
|
176
|
+
end)
|
|
177
|
+
|
|
178
|
+
-- Utility function to safely call Studio APIs
|
|
179
|
+
local function safeCall(func, ...)
|
|
180
|
+
local success, result = pcall(func, ...)
|
|
181
|
+
if success then
|
|
182
|
+
return result
|
|
183
|
+
else
|
|
184
|
+
warn("MCP Plugin Error: " .. tostring(result))
|
|
185
|
+
return nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
-- Instance path utility
|
|
190
|
+
local function getInstancePath(instance)
|
|
191
|
+
if not instance or instance == game then
|
|
192
|
+
return "game"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
local path = {}
|
|
196
|
+
local current = instance
|
|
197
|
+
|
|
198
|
+
while current and current ~= game do
|
|
199
|
+
table.insert(path, 1, current.Name)
|
|
200
|
+
current = current.Parent
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
return "game." .. table.concat(path, ".")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
-- Forward declarations
|
|
207
|
+
local processRequest
|
|
208
|
+
local sendResponse
|
|
209
|
+
local handlers = {}
|
|
210
|
+
|
|
211
|
+
-- Check for pending requests from MCP server
|
|
212
|
+
local function pollForRequests()
|
|
213
|
+
if not pluginState.isActive then
|
|
214
|
+
return
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
local success, result = pcall(function()
|
|
218
|
+
return HttpService:RequestAsync({
|
|
219
|
+
Url = pluginState.serverUrl .. "/poll",
|
|
220
|
+
Method = "GET",
|
|
221
|
+
Headers = {
|
|
222
|
+
["Content-Type"] = "application/json",
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
end)
|
|
226
|
+
|
|
227
|
+
if success and result.Success then
|
|
228
|
+
-- Update status to show successful connection
|
|
229
|
+
if statusLabel.Text == "Status: Connecting..." then
|
|
230
|
+
statusLabel.Text = "Status: Connected"
|
|
231
|
+
statusLabel.TextColor3 = Color3.fromRGB(85, 255, 85)
|
|
232
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(85, 255, 85)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
local data = HttpService:JSONDecode(result.Body)
|
|
236
|
+
if data.request then
|
|
237
|
+
-- Process the request and send response
|
|
238
|
+
local response = processRequest(data.request)
|
|
239
|
+
sendResponse(data.requestId, response)
|
|
240
|
+
end
|
|
241
|
+
elseif pluginState.isActive then
|
|
242
|
+
-- Connection failed, show error
|
|
243
|
+
statusLabel.Text = "Status: Connection Error"
|
|
244
|
+
statusLabel.TextColor3 = Color3.fromRGB(255, 200, 85)
|
|
245
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(255, 200, 85)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
-- Send response back to MCP server
|
|
250
|
+
sendResponse = function(requestId, responseData)
|
|
251
|
+
pcall(function()
|
|
252
|
+
HttpService:RequestAsync({
|
|
253
|
+
Url = pluginState.serverUrl .. "/response",
|
|
254
|
+
Method = "POST",
|
|
255
|
+
Headers = {
|
|
256
|
+
["Content-Type"] = "application/json",
|
|
257
|
+
},
|
|
258
|
+
Body = HttpService:JSONEncode({
|
|
259
|
+
requestId = requestId,
|
|
260
|
+
response = responseData,
|
|
261
|
+
}),
|
|
262
|
+
})
|
|
263
|
+
end)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
-- Process incoming requests
|
|
267
|
+
processRequest = function(request)
|
|
268
|
+
local endpoint = request.endpoint
|
|
269
|
+
local data = request.data or {}
|
|
270
|
+
|
|
271
|
+
-- Route to appropriate handler
|
|
272
|
+
if endpoint == "/api/file-tree" then
|
|
273
|
+
return handlers.getFileTree(data)
|
|
274
|
+
elseif endpoint == "/api/file-content" then
|
|
275
|
+
return handlers.getFileContent(data)
|
|
276
|
+
elseif endpoint == "/api/search-files" then
|
|
277
|
+
return handlers.searchFiles(data)
|
|
278
|
+
elseif endpoint == "/api/file-properties" then
|
|
279
|
+
return handlers.getFileProperties(data)
|
|
280
|
+
elseif endpoint == "/api/place-info" then
|
|
281
|
+
return handlers.getPlaceInfo(data)
|
|
282
|
+
elseif endpoint == "/api/services" then
|
|
283
|
+
return handlers.getServices(data)
|
|
284
|
+
elseif endpoint == "/api/selection" then
|
|
285
|
+
return handlers.getSelection(data)
|
|
286
|
+
elseif endpoint == "/api/search-objects" then
|
|
287
|
+
return handlers.searchObjects(data)
|
|
288
|
+
else
|
|
289
|
+
return { error = "Unknown endpoint: " .. tostring(endpoint) }
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
-- Get instance by path
|
|
294
|
+
local function getInstanceByPath(path)
|
|
295
|
+
if path == "game" or path == "" then
|
|
296
|
+
return game
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
-- Remove "game." prefix if present
|
|
300
|
+
path = path:gsub("^game%.", "")
|
|
301
|
+
|
|
302
|
+
local parts = {}
|
|
303
|
+
for part in path:gmatch("[^%.]+") do
|
|
304
|
+
table.insert(parts, part)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
local current = game
|
|
308
|
+
for _, part in ipairs(parts) do
|
|
309
|
+
current = current:FindFirstChild(part)
|
|
310
|
+
if not current then
|
|
311
|
+
return nil
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
return current
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
-- File System Tools Implementation
|
|
319
|
+
handlers.getFileTree = function(requestData)
|
|
320
|
+
local path = requestData.path or ""
|
|
321
|
+
local startInstance = getInstanceByPath(path)
|
|
322
|
+
|
|
323
|
+
if not startInstance then
|
|
324
|
+
return { error = "Path not found: " .. path }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
local function buildTree(instance, depth)
|
|
328
|
+
if depth > 10 then -- Prevent infinite recursion
|
|
329
|
+
return { name = instance.Name, className = instance.ClassName, children = {} }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
local node = {
|
|
333
|
+
name = instance.Name,
|
|
334
|
+
className = instance.ClassName,
|
|
335
|
+
path = getInstancePath(instance),
|
|
336
|
+
children = {},
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
-- Add source if it's a script
|
|
340
|
+
if instance:IsA("BaseScript") then
|
|
341
|
+
node.hasSource = true
|
|
342
|
+
node.scriptType = instance.ClassName
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
-- Add children
|
|
346
|
+
for _, child in ipairs(instance:GetChildren()) do
|
|
347
|
+
table.insert(node.children, buildTree(child, depth + 1))
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
return node
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
tree = buildTree(startInstance, 0),
|
|
355
|
+
timestamp = tick(),
|
|
356
|
+
}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
handlers.getFileContent = function(requestData)
|
|
360
|
+
local path = requestData.path
|
|
361
|
+
if not path then
|
|
362
|
+
return { error = "Path is required" }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
local instance = getInstanceByPath(path)
|
|
366
|
+
if not instance then
|
|
367
|
+
return { error = "Instance not found: " .. path }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
if not instance:IsA("BaseScript") then
|
|
371
|
+
return { error = "Instance is not a script: " .. path }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
path = path,
|
|
376
|
+
source = instance.Source,
|
|
377
|
+
className = instance.ClassName,
|
|
378
|
+
name = instance.Name,
|
|
379
|
+
}
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
handlers.searchFiles = function(requestData)
|
|
383
|
+
local query = requestData.query
|
|
384
|
+
local searchType = requestData.searchType or "name"
|
|
385
|
+
|
|
386
|
+
if not query then
|
|
387
|
+
return { error = "Query is required" }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
local results = {}
|
|
391
|
+
|
|
392
|
+
local function searchRecursive(instance)
|
|
393
|
+
local match = false
|
|
394
|
+
|
|
395
|
+
if searchType == "name" then
|
|
396
|
+
match = instance.Name:lower():find(query:lower()) ~= nil
|
|
397
|
+
elseif searchType == "type" then
|
|
398
|
+
match = instance.ClassName:lower():find(query:lower()) ~= nil
|
|
399
|
+
elseif searchType == "content" and instance:IsA("BaseScript") then
|
|
400
|
+
match = instance.Source:lower():find(query:lower()) ~= nil
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
if match then
|
|
404
|
+
table.insert(results, {
|
|
405
|
+
name = instance.Name,
|
|
406
|
+
className = instance.ClassName,
|
|
407
|
+
path = getInstancePath(instance),
|
|
408
|
+
hasSource = instance:IsA("BaseScript"),
|
|
409
|
+
})
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
for _, child in ipairs(instance:GetChildren()) do
|
|
413
|
+
searchRecursive(child)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
searchRecursive(game)
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
results = results,
|
|
421
|
+
query = query,
|
|
422
|
+
searchType = searchType,
|
|
423
|
+
count = #results,
|
|
424
|
+
}
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
handlers.getFileProperties = function(requestData)
|
|
428
|
+
local path = requestData.path
|
|
429
|
+
if not path then
|
|
430
|
+
return { error = "Path is required" }
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
local instance = getInstanceByPath(path)
|
|
434
|
+
if not instance then
|
|
435
|
+
return { error = "Instance not found: " .. path }
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
local properties = {}
|
|
439
|
+
local success, result = pcall(function()
|
|
440
|
+
-- Get basic properties
|
|
441
|
+
properties.Name = instance.Name
|
|
442
|
+
properties.ClassName = instance.ClassName
|
|
443
|
+
properties.Parent = instance.Parent and getInstancePath(instance.Parent) or "nil"
|
|
444
|
+
|
|
445
|
+
-- Get children count
|
|
446
|
+
properties.ChildCount = #instance:GetChildren()
|
|
447
|
+
|
|
448
|
+
-- Script-specific properties
|
|
449
|
+
if instance:IsA("BaseScript") then
|
|
450
|
+
properties.Source = instance.Source
|
|
451
|
+
properties.Enabled = instance.Enabled
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
return properties
|
|
455
|
+
end)
|
|
456
|
+
|
|
457
|
+
if success then
|
|
458
|
+
return {
|
|
459
|
+
path = path,
|
|
460
|
+
properties = properties,
|
|
461
|
+
}
|
|
462
|
+
else
|
|
463
|
+
return { error = "Failed to get properties: " .. tostring(result) }
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
-- Studio Context Tools Implementation
|
|
468
|
+
handlers.getPlaceInfo = function(requestData)
|
|
469
|
+
return {
|
|
470
|
+
placeName = game.Name,
|
|
471
|
+
placeId = game.PlaceId,
|
|
472
|
+
gameId = game.GameId,
|
|
473
|
+
jobId = game.JobId,
|
|
474
|
+
workspace = {
|
|
475
|
+
name = workspace.Name,
|
|
476
|
+
className = workspace.ClassName,
|
|
477
|
+
},
|
|
478
|
+
}
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
handlers.getServices = function(requestData)
|
|
482
|
+
local serviceName = requestData.serviceName
|
|
483
|
+
|
|
484
|
+
if serviceName then
|
|
485
|
+
local service = safeCall(game.GetService, game, serviceName)
|
|
486
|
+
if service then
|
|
487
|
+
return {
|
|
488
|
+
service = {
|
|
489
|
+
name = service.Name,
|
|
490
|
+
className = service.ClassName,
|
|
491
|
+
path = getInstancePath(service),
|
|
492
|
+
childCount = #service:GetChildren(),
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
else
|
|
496
|
+
return { error = "Service not found: " .. serviceName }
|
|
497
|
+
end
|
|
498
|
+
else
|
|
499
|
+
-- Return common services
|
|
500
|
+
local services = {}
|
|
501
|
+
local commonServices = {
|
|
502
|
+
"Workspace",
|
|
503
|
+
"Players",
|
|
504
|
+
"StarterGui",
|
|
505
|
+
"StarterPack",
|
|
506
|
+
"StarterPlayer",
|
|
507
|
+
"ReplicatedStorage",
|
|
508
|
+
"ServerStorage",
|
|
509
|
+
"ServerScriptService",
|
|
510
|
+
"HttpService",
|
|
511
|
+
"TeleportService",
|
|
512
|
+
"DataStoreService",
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for _, serviceName in ipairs(commonServices) do
|
|
516
|
+
local service = safeCall(game.GetService, game, serviceName)
|
|
517
|
+
if service then
|
|
518
|
+
table.insert(services, {
|
|
519
|
+
name = service.Name,
|
|
520
|
+
className = service.ClassName,
|
|
521
|
+
path = getInstancePath(service),
|
|
522
|
+
childCount = #service:GetChildren(),
|
|
523
|
+
})
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
return { services = services }
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
handlers.getSelection = function(requestData)
|
|
532
|
+
local selected = Selection:Get()
|
|
533
|
+
local selection = {}
|
|
534
|
+
|
|
535
|
+
for _, instance in ipairs(selected) do
|
|
536
|
+
table.insert(selection, {
|
|
537
|
+
name = instance.Name,
|
|
538
|
+
className = instance.ClassName,
|
|
539
|
+
path = getInstancePath(instance),
|
|
540
|
+
})
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
selection = selection,
|
|
545
|
+
count = #selection,
|
|
546
|
+
}
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
handlers.searchObjects = function(requestData)
|
|
550
|
+
local query = requestData.query
|
|
551
|
+
local searchType = requestData.searchType or "name"
|
|
552
|
+
local propertyName = requestData.propertyName
|
|
553
|
+
|
|
554
|
+
if not query then
|
|
555
|
+
return { error = "Query is required" }
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
local results = {}
|
|
559
|
+
|
|
560
|
+
local function searchRecursive(instance)
|
|
561
|
+
local match = false
|
|
562
|
+
|
|
563
|
+
if searchType == "name" then
|
|
564
|
+
match = instance.Name:lower():find(query:lower()) ~= nil
|
|
565
|
+
elseif searchType == "class" then
|
|
566
|
+
match = instance.ClassName:lower():find(query:lower()) ~= nil
|
|
567
|
+
elseif searchType == "property" and propertyName then
|
|
568
|
+
local success, value = pcall(function()
|
|
569
|
+
return tostring(instance[propertyName])
|
|
570
|
+
end)
|
|
571
|
+
if success then
|
|
572
|
+
match = value:lower():find(query:lower()) ~= nil
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
if match then
|
|
577
|
+
table.insert(results, {
|
|
578
|
+
name = instance.Name,
|
|
579
|
+
className = instance.ClassName,
|
|
580
|
+
path = getInstancePath(instance),
|
|
581
|
+
})
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
for _, child in ipairs(instance:GetChildren()) do
|
|
585
|
+
searchRecursive(child)
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
searchRecursive(game)
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
results = results,
|
|
593
|
+
query = query,
|
|
594
|
+
searchType = searchType,
|
|
595
|
+
count = #results,
|
|
596
|
+
}
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
-- Update UI state
|
|
600
|
+
local function updateUIState()
|
|
601
|
+
if pluginState.isActive then
|
|
602
|
+
-- Connecting/Connected state
|
|
603
|
+
statusLabel.Text = "Status: Connecting..."
|
|
604
|
+
statusLabel.TextColor3 = Color3.fromRGB(255, 200, 85)
|
|
605
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(255, 200, 85)
|
|
606
|
+
connectButton.Text = "Disconnect"
|
|
607
|
+
if not buttonHover then
|
|
608
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(255, 85, 85)
|
|
609
|
+
end
|
|
610
|
+
urlInput.TextEditable = false
|
|
611
|
+
urlInput.BackgroundColor3 = Color3.fromRGB(40, 40, 40)
|
|
612
|
+
else
|
|
613
|
+
-- Disconnected state
|
|
614
|
+
statusLabel.Text = "Status: Disconnected"
|
|
615
|
+
statusLabel.TextColor3 = Color3.fromRGB(255, 85, 85)
|
|
616
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(255, 85, 85)
|
|
617
|
+
connectButton.Text = "Connect"
|
|
618
|
+
if not buttonHover then
|
|
619
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(0, 162, 255)
|
|
620
|
+
end
|
|
621
|
+
urlInput.TextEditable = true
|
|
622
|
+
urlInput.BackgroundColor3 = Color3.fromRGB(60, 60, 60)
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
-- Plugin activation/deactivation
|
|
627
|
+
local function activatePlugin()
|
|
628
|
+
-- Update server URL from input
|
|
629
|
+
pluginState.serverUrl = urlInput.Text
|
|
630
|
+
|
|
631
|
+
pluginState.isActive = true
|
|
632
|
+
screenGui.Enabled = true
|
|
633
|
+
updateUIState()
|
|
634
|
+
print("MCP Plugin: Activated - Server URL: " .. pluginState.serverUrl)
|
|
635
|
+
|
|
636
|
+
-- Start polling for requests
|
|
637
|
+
if not pluginState.connection then
|
|
638
|
+
pluginState.connection = RunService.Heartbeat:Connect(function()
|
|
639
|
+
local now = tick()
|
|
640
|
+
if now - pluginState.lastPoll > pluginState.pollInterval then
|
|
641
|
+
pluginState.lastPoll = now
|
|
642
|
+
pollForRequests()
|
|
643
|
+
end
|
|
644
|
+
end)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
local function deactivatePlugin()
|
|
649
|
+
pluginState.isActive = false
|
|
650
|
+
updateUIState()
|
|
651
|
+
print("MCP Plugin: Deactivated")
|
|
652
|
+
|
|
653
|
+
-- Stop polling
|
|
654
|
+
if pluginState.connection then
|
|
655
|
+
pluginState.connection:Disconnect()
|
|
656
|
+
pluginState.connection = nil
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
-- Connect button click handler
|
|
661
|
+
connectButton.Activated:Connect(function()
|
|
662
|
+
if pluginState.isActive then
|
|
663
|
+
deactivatePlugin()
|
|
664
|
+
else
|
|
665
|
+
activatePlugin()
|
|
666
|
+
end
|
|
667
|
+
end)
|
|
668
|
+
|
|
669
|
+
-- Toolbar button click handler (shows/hides UI)
|
|
670
|
+
button.Click:Connect(function()
|
|
671
|
+
screenGui.Enabled = not screenGui.Enabled
|
|
672
|
+
end)
|
|
673
|
+
|
|
674
|
+
-- Plugin unloading
|
|
675
|
+
plugin.Unloading:Connect(function()
|
|
676
|
+
deactivatePlugin()
|
|
677
|
+
end)
|
|
678
|
+
|
|
679
|
+
-- Initialize UI state
|
|
680
|
+
updateUIState()
|
|
681
|
+
|
|
682
|
+
print("Roblox Studio MCP Plugin loaded successfully!")
|
|
683
|
+
print("Click the MCP Server button in the toolbar to open the interface")
|