robloxstudio-mcp 2.7.0-next.3 → 2.7.0-next.5
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/dist/index.js
CHANGED
|
@@ -158,7 +158,7 @@ var TOOL_HANDLERS = {
|
|
|
158
158
|
}),
|
|
159
159
|
get_script_source: (tools, body) => tools.getScriptSource(body.instancePath, body.startLine, body.endLine),
|
|
160
160
|
set_script_source: (tools, body) => tools.setScriptSource(body.instancePath, body.source),
|
|
161
|
-
edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string),
|
|
161
|
+
edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string, body.startLine),
|
|
162
162
|
insert_script_lines: (tools, body) => tools.insertScriptLines(body.instancePath, body.afterLine, body.newContent),
|
|
163
163
|
delete_script_lines: (tools, body) => tools.deleteScriptLines(body.instancePath, body.startLine, body.endLine),
|
|
164
164
|
get_attribute: (tools, body) => tools.getAttribute(body.instancePath, body.attributeName),
|
|
@@ -1629,11 +1629,14 @@ ${code}`
|
|
|
1629
1629
|
]
|
|
1630
1630
|
};
|
|
1631
1631
|
}
|
|
1632
|
-
async editScriptLines(instancePath, oldString, newString) {
|
|
1632
|
+
async editScriptLines(instancePath, oldString, newString, startLine) {
|
|
1633
1633
|
if (!instancePath || typeof oldString !== "string" || typeof newString !== "string") {
|
|
1634
1634
|
throw new Error("Instance path, old_string, and new_string are required for edit_script_lines");
|
|
1635
1635
|
}
|
|
1636
|
-
const
|
|
1636
|
+
const payload = { instancePath, old_string: oldString, new_string: newString };
|
|
1637
|
+
if (startLine !== void 0)
|
|
1638
|
+
payload.startLine = startLine;
|
|
1639
|
+
const response = await this.client.request("/api/edit-script-lines", payload);
|
|
1637
1640
|
return {
|
|
1638
1641
|
content: [
|
|
1639
1642
|
{
|
|
@@ -2496,6 +2499,18 @@ ${code}`
|
|
|
2496
2499
|
}]
|
|
2497
2500
|
};
|
|
2498
2501
|
}
|
|
2502
|
+
async resolveImageId(decalAssetId) {
|
|
2503
|
+
try {
|
|
2504
|
+
const resp = await fetch(`https://economy.roblox.com/v2/assets/${decalAssetId}/details`);
|
|
2505
|
+
if (!resp.ok)
|
|
2506
|
+
return decalAssetId;
|
|
2507
|
+
const details = await resp.json();
|
|
2508
|
+
if (details.TextureId)
|
|
2509
|
+
return String(details.TextureId);
|
|
2510
|
+
} catch {
|
|
2511
|
+
}
|
|
2512
|
+
return decalAssetId;
|
|
2513
|
+
}
|
|
2499
2514
|
async uploadDecal(filePath, displayName, description, userId, groupId) {
|
|
2500
2515
|
if (!fs.existsSync(filePath)) {
|
|
2501
2516
|
throw new Error(`File not found: ${filePath}`);
|
|
@@ -2517,10 +2532,15 @@ ${code}`
|
|
|
2517
2532
|
description: description || "",
|
|
2518
2533
|
creationContext: { creator }
|
|
2519
2534
|
}, fileContent, fileName);
|
|
2535
|
+
const decalId = result.response?.assetId;
|
|
2536
|
+
const imageId = decalId ? await this.resolveImageId(decalId) : void 0;
|
|
2520
2537
|
return {
|
|
2521
2538
|
content: [{
|
|
2522
2539
|
type: "text",
|
|
2523
|
-
text: JSON.stringify(
|
|
2540
|
+
text: JSON.stringify({
|
|
2541
|
+
...result,
|
|
2542
|
+
imageId
|
|
2543
|
+
})
|
|
2524
2544
|
}]
|
|
2525
2545
|
};
|
|
2526
2546
|
}
|
|
@@ -2535,7 +2555,8 @@ ${code}`
|
|
|
2535
2555
|
assetId: String(result.assetId),
|
|
2536
2556
|
displayName,
|
|
2537
2557
|
assetType: "Decal",
|
|
2538
|
-
backingAssetId: String(result.backingAssetId)
|
|
2558
|
+
backingAssetId: String(result.backingAssetId),
|
|
2559
|
+
imageId: String(result.backingAssetId)
|
|
2539
2560
|
}
|
|
2540
2561
|
})
|
|
2541
2562
|
}]
|
|
@@ -3013,7 +3034,7 @@ var RobloxStudioMCPServer = class {
|
|
|
3013
3034
|
case "set_script_source":
|
|
3014
3035
|
return await this.tools.setScriptSource(args?.instancePath, args?.source);
|
|
3015
3036
|
case "edit_script_lines":
|
|
3016
|
-
return await this.tools.editScriptLines(args?.instancePath, args?.old_string, args?.new_string);
|
|
3037
|
+
return await this.tools.editScriptLines(args?.instancePath, args?.old_string, args?.new_string, args?.startLine);
|
|
3017
3038
|
case "insert_script_lines":
|
|
3018
3039
|
return await this.tools.insertScriptLines(args?.instancePath, args?.afterLine, args?.newContent);
|
|
3019
3040
|
case "delete_script_lines":
|
|
@@ -3766,7 +3787,7 @@ var TOOL_DEFINITIONS = [
|
|
|
3766
3787
|
{
|
|
3767
3788
|
name: "edit_script_lines",
|
|
3768
3789
|
category: "write",
|
|
3769
|
-
description: "Replace exact text in a script. old_string must match exactly once in the script (
|
|
3790
|
+
description: "Replace exact text in a script. Without startLine, old_string must match exactly once in the script. Pass startLine (1-indexed, from get_script_source) to anchor the edit to a specific line when old_string is ambiguous (e.g. repeated closing braces).",
|
|
3770
3791
|
inputSchema: {
|
|
3771
3792
|
type: "object",
|
|
3772
3793
|
properties: {
|
|
@@ -3776,11 +3797,15 @@ var TOOL_DEFINITIONS = [
|
|
|
3776
3797
|
},
|
|
3777
3798
|
old_string: {
|
|
3778
3799
|
type: "string",
|
|
3779
|
-
description: "Exact text to find and replace
|
|
3800
|
+
description: "Exact text to find and replace. Must be unique in the script unless startLine is provided."
|
|
3780
3801
|
},
|
|
3781
3802
|
new_string: {
|
|
3782
3803
|
type: "string",
|
|
3783
3804
|
description: "Replacement text"
|
|
3805
|
+
},
|
|
3806
|
+
startLine: {
|
|
3807
|
+
type: "number",
|
|
3808
|
+
description: "Optional 1-indexed line where old_string begins. When provided, skips uniqueness check and requires old_string to match starting at that exact line."
|
|
3784
3809
|
}
|
|
3785
3810
|
},
|
|
3786
3811
|
required: ["instancePath", "old_string", "new_string"]
|
package/package.json
CHANGED
|
@@ -4429,11 +4429,11 @@ local function setScriptSource(requestData)
|
|
|
4429
4429
|
error = `Failed to set script source. UpdateSourceAsync failed: {updateResult}. Direct assignment failed: {directResult}. Replace method failed: {replaceResult}`,
|
|
4430
4430
|
}
|
|
4431
4431
|
end
|
|
4432
|
-
local escapeLuaPattern, escapeLuaReplacement
|
|
4433
4432
|
local function editScriptLines(requestData)
|
|
4434
4433
|
local instancePath = requestData.instancePath
|
|
4435
4434
|
local oldString = requestData.old_string
|
|
4436
4435
|
local newString = requestData.new_string
|
|
4436
|
+
local startLine = requestData.startLine
|
|
4437
4437
|
if not (instancePath ~= "" and instancePath) or oldString == nil or newString == nil then
|
|
4438
4438
|
return {
|
|
4439
4439
|
error = "Instance path, old_string, and new_string are required",
|
|
@@ -4455,31 +4455,55 @@ local function editScriptLines(requestData)
|
|
|
4455
4455
|
local recordingId = beginRecording(`Edit script: {instance.Name}`)
|
|
4456
4456
|
local success, result = pcall(function()
|
|
4457
4457
|
local source = readScriptSource(instance)
|
|
4458
|
-
-- Count occurrences to ensure uniqueness
|
|
4459
|
-
local count = 0
|
|
4460
|
-
local searchPos = 1
|
|
4461
4458
|
local searchLen = #oldString
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
if
|
|
4465
|
-
|
|
4459
|
+
local matchStart
|
|
4460
|
+
if startLine ~= nil then
|
|
4461
|
+
if startLine < 1 then
|
|
4462
|
+
error(`startLine must be >= 1 (got {startLine})`)
|
|
4463
|
+
end
|
|
4464
|
+
local lineStartByte = 1
|
|
4465
|
+
local currentLine = 1
|
|
4466
|
+
while currentLine < startLine do
|
|
4467
|
+
local nlPos = string.find(source, "\n", lineStartByte, true)
|
|
4468
|
+
if nlPos == nil then
|
|
4469
|
+
error(`startLine {startLine} is past end of script ({currentLine} lines)`)
|
|
4470
|
+
end
|
|
4471
|
+
lineStartByte = nlPos + 1
|
|
4472
|
+
currentLine += 1
|
|
4473
|
+
end
|
|
4474
|
+
local candidate = string.sub(source, lineStartByte, lineStartByte + searchLen - 1)
|
|
4475
|
+
if candidate ~= oldString then
|
|
4476
|
+
error(`old_string does not match at line {startLine}. Use get_script_source to verify the exact text at that line.`)
|
|
4477
|
+
end
|
|
4478
|
+
matchStart = lineStartByte
|
|
4479
|
+
else
|
|
4480
|
+
local count = 0
|
|
4481
|
+
local searchPos = 1
|
|
4482
|
+
local firstMatch
|
|
4483
|
+
while true do
|
|
4484
|
+
local foundStart = string.find(source, oldString, searchPos, true)
|
|
4485
|
+
if foundStart == nil then
|
|
4486
|
+
break
|
|
4487
|
+
end
|
|
4488
|
+
if firstMatch == nil then
|
|
4489
|
+
firstMatch = foundStart
|
|
4490
|
+
end
|
|
4491
|
+
count += 1
|
|
4492
|
+
if count > 1 then
|
|
4493
|
+
break
|
|
4494
|
+
end
|
|
4495
|
+
searchPos = foundStart + searchLen
|
|
4496
|
+
end
|
|
4497
|
+
if count == 0 then
|
|
4498
|
+
error("old_string not found in script. If old_string contains repeated patterns (e.g. closing braces), pass startLine to anchor the edit.")
|
|
4466
4499
|
end
|
|
4467
|
-
count += 1
|
|
4468
4500
|
if count > 1 then
|
|
4469
|
-
|
|
4501
|
+
error("old_string matches multiple locations. Provide more surrounding context, or pass startLine to anchor the edit to a specific line.")
|
|
4470
4502
|
end
|
|
4471
|
-
|
|
4472
|
-
end
|
|
4473
|
-
if count == 0 then
|
|
4474
|
-
error("old_string not found in script")
|
|
4475
|
-
end
|
|
4476
|
-
if count > 1 then
|
|
4477
|
-
error("old_string matches multiple locations. Provide more surrounding context to make it unique")
|
|
4503
|
+
matchStart = firstMatch
|
|
4478
4504
|
end
|
|
4479
|
-
--
|
|
4480
|
-
local
|
|
4481
|
-
local escapedRepl = escapeLuaReplacement(newString)
|
|
4482
|
-
local newSource = string.gsub(source, escaped, escapedRepl, 1)
|
|
4505
|
+
-- Byte-slice replacement avoids Lua pattern escaping (safe for multi-byte chars like em dashes).
|
|
4506
|
+
local newSource = string.sub(source, 1, matchStart - 1) .. newString .. string.sub(source, matchStart + searchLen)
|
|
4483
4507
|
ScriptEditorService:UpdateSourceAsync(instance, function()
|
|
4484
4508
|
return newSource
|
|
4485
4509
|
end)
|
|
@@ -4677,10 +4701,10 @@ local function deleteScriptLines(requestData)
|
|
|
4677
4701
|
error = `Failed to delete script lines: {result}`,
|
|
4678
4702
|
}
|
|
4679
4703
|
end
|
|
4680
|
-
function escapeLuaPattern(s)
|
|
4704
|
+
local function escapeLuaPattern(s)
|
|
4681
4705
|
return (string.gsub(s, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
|
|
4682
4706
|
end
|
|
4683
|
-
function escapeLuaReplacement(s)
|
|
4707
|
+
local function escapeLuaReplacement(s)
|
|
4684
4708
|
return (string.gsub(s, "%%", "%%%%"))
|
|
4685
4709
|
end
|
|
4686
4710
|
local function caseInsensitiveLiteralReplace(src, searchStr, repl)
|
|
@@ -5273,7 +5297,7 @@ return {
|
|
|
5273
5297
|
<Properties>
|
|
5274
5298
|
<string name="Name">State</string>
|
|
5275
5299
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5276
|
-
local CURRENT_VERSION = "2.7.0-next.
|
|
5300
|
+
local CURRENT_VERSION = "2.7.0-next.5"
|
|
5277
5301
|
local MAX_CONNECTIONS = 5
|
|
5278
5302
|
local BASE_PORT = 58741
|
|
5279
5303
|
local activeTabIndex = 0
|
|
@@ -203,6 +203,7 @@ function editScriptLines(requestData: Record<string, unknown>) {
|
|
|
203
203
|
const instancePath = requestData.instancePath as string;
|
|
204
204
|
let oldString = requestData.old_string as string;
|
|
205
205
|
let newString = requestData.new_string as string;
|
|
206
|
+
const startLine = requestData.startLine as number | undefined;
|
|
206
207
|
|
|
207
208
|
if (!instancePath || oldString === undefined || newString === undefined) {
|
|
208
209
|
return { error: "Instance path, old_string, and new_string are required" };
|
|
@@ -221,27 +222,47 @@ function editScriptLines(requestData: Record<string, unknown>) {
|
|
|
221
222
|
|
|
222
223
|
const [success, result] = pcall(() => {
|
|
223
224
|
const source = readScriptSource(instance);
|
|
224
|
-
|
|
225
|
-
// Count occurrences to ensure uniqueness
|
|
226
|
-
let count = 0;
|
|
227
|
-
let searchPos = 1;
|
|
228
225
|
const searchLen = oldString.size();
|
|
226
|
+
let matchStart: number;
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (foundStart === undefined) break;
|
|
233
|
-
count++;
|
|
234
|
-
if (count > 1) break;
|
|
235
|
-
searchPos = foundStart + searchLen;
|
|
236
|
-
}
|
|
228
|
+
if (startLine !== undefined) {
|
|
229
|
+
if (startLine < 1) error(`startLine must be >= 1 (got ${startLine})`);
|
|
237
230
|
|
|
238
|
-
|
|
239
|
-
|
|
231
|
+
let lineStartByte = 1;
|
|
232
|
+
let currentLine = 1;
|
|
233
|
+
while (currentLine < startLine) {
|
|
234
|
+
const [nlPos] = string.find(source, "\n", lineStartByte, true);
|
|
235
|
+
if (nlPos === undefined) {
|
|
236
|
+
error(`startLine ${startLine} is past end of script (${currentLine} lines)`);
|
|
237
|
+
}
|
|
238
|
+
lineStartByte = (nlPos as number) + 1;
|
|
239
|
+
currentLine++;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const candidate = string.sub(source, lineStartByte, lineStartByte + searchLen - 1);
|
|
243
|
+
if (candidate !== oldString) {
|
|
244
|
+
error(`old_string does not match at line ${startLine}. Use get_script_source to verify the exact text at that line.`);
|
|
245
|
+
}
|
|
246
|
+
matchStart = lineStartByte;
|
|
247
|
+
} else {
|
|
248
|
+
let count = 0;
|
|
249
|
+
let searchPos = 1;
|
|
250
|
+
let firstMatch: number | undefined;
|
|
251
|
+
while (true) {
|
|
252
|
+
const [foundStart] = string.find(source, oldString, searchPos, true);
|
|
253
|
+
if (foundStart === undefined) break;
|
|
254
|
+
if (firstMatch === undefined) firstMatch = foundStart;
|
|
255
|
+
count++;
|
|
256
|
+
if (count > 1) break;
|
|
257
|
+
searchPos = foundStart + searchLen;
|
|
258
|
+
}
|
|
259
|
+
if (count === 0) error("old_string not found in script. If old_string contains repeated patterns (e.g. closing braces), pass startLine to anchor the edit.");
|
|
260
|
+
if (count > 1) error("old_string matches multiple locations. Provide more surrounding context, or pass startLine to anchor the edit to a specific line.");
|
|
261
|
+
matchStart = firstMatch as number;
|
|
262
|
+
}
|
|
240
263
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
const escapedRepl = escapeLuaReplacement(newString);
|
|
244
|
-
const [newSource] = string.gsub(source, escaped, escapedRepl, 1);
|
|
264
|
+
// Byte-slice replacement avoids Lua pattern escaping (safe for multi-byte chars like em dashes).
|
|
265
|
+
const newSource = string.sub(source, 1, matchStart - 1) + newString + string.sub(source, matchStart + searchLen);
|
|
245
266
|
|
|
246
267
|
ScriptEditorService.UpdateSourceAsync(instance, () => newSource);
|
|
247
268
|
|