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.
@@ -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")