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 response = await this.client.request("/api/edit-script-lines", { instancePath, old_string: oldString, new_string: newString });
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(result)
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 (whitespace-sensitive). Use get_script_source first to see current content.",
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 (must be unique in the script)"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robloxstudio-mcp",
3
- "version": "2.7.0-next.3",
3
+ "version": "2.7.0-next.5",
4
4
  "description": "MCP Server for Roblox Studio Integration - Access Studio data, scripts, and objects through AI tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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
- while true do
4463
- local foundStart = string.find(source, oldString, searchPos, true)
4464
- if foundStart == nil then
4465
- break
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
- break
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
- searchPos = foundStart + searchLen
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
- -- Perform the replacement (plain literal find + replace)
4480
- local escaped = escapeLuaPattern(oldString)
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.2"
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
- while (true) {
231
- const [foundStart] = string.find(source, oldString, searchPos, true);
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
- if (count === 0) error("old_string not found in script");
239
- if (count > 1) error("old_string matches multiple locations. Provide more surrounding context to make it unique");
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
- // Perform the replacement (plain literal find + replace)
242
- const escaped = escapeLuaPattern(oldString);
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