Files
server-deploy/windows-dev-stack/godot-mcp-pro-v1.14.1/addons/godot_mcp/commands/script_commands.gd
Joywayer dd3eb24d0f refactor: 拆分 claude-dev-stack 为 windows-dev-stack 和 wsl-dev-stack
将原 claude-dev-stack 目录拆分为独立的 Windows 和 WSL 部署栈,便于分别维护和使用。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 01:11:20 +08:00

370 lines
11 KiB
GDScript

@tool
extends "res://addons/godot_mcp/commands/base_command.gd"
func get_commands() -> Dictionary:
return {
"list_scripts": _list_scripts,
"read_script": _read_script,
"create_script": _create_script,
"edit_script": _edit_script,
"attach_script": _attach_script,
"get_open_scripts": _get_open_scripts,
"validate_script": _validate_script,
}
func _guard_script_file_path(path: String, operation: String) -> Dictionary:
var ext := path.get_extension().to_lower()
if ext in ["gd", "cs"]:
return {}
return error(
-32602,
"%s only supports script files (.gd, .cs): %s" % [operation, normalize_project_path(path)],
{
"path": normalize_project_path(path),
"extension": ext,
"suggestion": "Use scene commands for .tscn/.scn files and shader commands for shader resources.",
}
)
func _list_scripts(params: Dictionary) -> Dictionary:
var path: String = optional_string(params, "path", "res://")
var recursive: bool = optional_bool(params, "recursive", true)
var scripts: Array = []
_find_scripts(path, recursive, scripts)
return success({"scripts": scripts, "count": scripts.size()})
func _find_scripts(path: String, recursive: bool, scripts: Array) -> void:
var dir := DirAccess.open(path)
if dir == null:
return
dir.list_dir_begin()
var file_name := dir.get_next()
while not file_name.is_empty():
if file_name.begins_with("."):
file_name = dir.get_next()
continue
var full_path := path.path_join(file_name)
if dir.current_is_dir():
if recursive:
_find_scripts(full_path, recursive, scripts)
elif file_name.get_extension() in ["gd", "cs", "gdshader"]:
var info := {"path": full_path, "type": file_name.get_extension()}
# Get basic file info
var file := FileAccess.open(full_path, FileAccess.READ)
if file:
info["size"] = file.get_length()
# Read first line for class/extends info
var first_line := file.get_line().strip_edges()
if first_line.begins_with("class_name "):
info["class_name"] = first_line.substr(11).strip_edges()
elif first_line.begins_with("extends "):
info["extends"] = first_line.substr(8).strip_edges()
file.close()
scripts.append(info)
file_name = dir.get_next()
dir.list_dir_end()
func _read_script(params: Dictionary) -> Dictionary:
var result := require_string(params, "path")
if result[1] != null:
return result[1]
var path: String = result[0]
if not FileAccess.file_exists(path):
return error_not_found("Script '%s'" % path)
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return error_internal("Cannot read script: %s" % error_string(FileAccess.get_open_error()))
var content := file.get_as_text()
var line_count := content.count("\n") + 1
file.close()
return success({
"path": path,
"content": content,
"line_count": line_count,
"size": content.length(),
})
func _create_script(params: Dictionary) -> Dictionary:
var result := require_string(params, "path")
if result[1] != null:
return result[1]
var path: String = result[0]
var path_guard := _guard_script_file_path(path, "create_script")
if not path_guard.is_empty():
return path_guard
var content: String = optional_string(params, "content", "")
var base_class: String = optional_string(params, "extends", "Node")
var class_name_str: String = optional_string(params, "class_name", "")
var force: bool = optional_bool(params, "force", false)
var guard := guard_text_resource_write(path, force)
if not guard.is_empty():
return guard
# Generate template if no content provided
if content.is_empty():
var lines: PackedStringArray = []
if not class_name_str.is_empty():
lines.append("class_name %s" % class_name_str)
lines.append("extends %s" % base_class)
lines.append("")
lines.append("")
lines.append("func _ready() -> void:")
lines.append("\tpass")
lines.append("")
content = "\n".join(lines)
# Ensure directory exists
var dir_path := path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir_path):
DirAccess.make_dir_recursive_absolute(dir_path)
var file := FileAccess.open(path, FileAccess.WRITE)
if file == null:
return error_internal("Cannot create script: %s" % error_string(FileAccess.get_open_error()))
file.store_string(content)
file.close()
EditorInterface.get_resource_filesystem().scan()
# Pre-load so the script is available immediately
if ResourceLoader.exists(path):
var script = load(path)
if script is Script:
script.reload(true)
return success({"path": path, "created": true})
func _edit_script(params: Dictionary) -> Dictionary:
var result := require_string(params, "path")
if result[1] != null:
return result[1]
var path: String = result[0]
var path_guard := _guard_script_file_path(path, "edit_script")
if not path_guard.is_empty():
return path_guard
if not FileAccess.file_exists(path):
return error_not_found("Script '%s'" % path)
var force: bool = optional_bool(params, "force", false)
var guard := guard_text_resource_write(path, force)
if not guard.is_empty():
return guard
# Read current content
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return error_internal("Cannot read script: %s" % error_string(FileAccess.get_open_error()))
var content := file.get_as_text()
file.close()
var changes_made := 0
# Support search-and-replace
if params.has("replacements") and params["replacements"] is Array:
var replacements: Array = params["replacements"]
for replacement in replacements:
if replacement is Dictionary:
var search: String = replacement.get("search", "")
var replace: String = replacement.get("replace", "")
if not search.is_empty():
var use_regex: bool = replacement.get("regex", false)
if use_regex:
var regex := RegEx.new()
var err := regex.compile(search)
if err == OK:
var new_content := regex.sub(content, replace, true)
if new_content != content:
content = new_content
changes_made += 1
else:
if content.contains(search):
content = content.replace(search, replace)
changes_made += 1
# Support 1-based inclusive line range replacement
elif params.has("content") and (params.has("start_line") or params.has("end_line")):
if not params.has("start_line"):
return error_invalid_params("start_line is required when end_line is provided")
var start_line: int = int(params["start_line"])
var end_line: int = int(params.get("end_line", start_line))
var lines := content.split("\n")
if start_line < 1:
return error_invalid_params("start_line must be >= 1")
if end_line < start_line:
return error_invalid_params("end_line must be >= start_line")
if start_line > lines.size():
return error_invalid_params("start_line is beyond the end of the file")
if end_line > lines.size():
return error_invalid_params("end_line is beyond the end of the file")
var replacement_lines := str(params["content"]).split("\n")
var start_index := start_line - 1
var remove_count := end_line - start_line + 1
for _i in range(remove_count):
lines.remove_at(start_index)
for i in range(replacement_lines.size()):
lines.insert(start_index + i, replacement_lines[i])
content = "\n".join(lines)
changes_made = 1
# Support full content replacement
elif params.has("content"):
content = str(params["content"])
changes_made = 1
# Support insert at line
elif params.has("insert_at_line") and params.has("text"):
var line_num: int = int(params["insert_at_line"])
var text: String = str(params["text"])
var lines := content.split("\n")
line_num = clampi(line_num, 0, lines.size())
lines.insert(line_num, text)
content = "\n".join(lines)
changes_made = 1
if changes_made == 0:
return success({"path": path, "changes_made": 0, "message": "No changes applied"})
# Write back
file = FileAccess.open(path, FileAccess.WRITE)
if file == null:
return error_internal("Cannot write script: %s" % error_string(FileAccess.get_open_error()))
file.store_string(content)
file.close()
# Reload the script resource so the editor picks up changes immediately
_reload_script(path)
return success({"path": path, "changes_made": changes_made})
## Force-reload a script so the editor reflects disk changes immediately.
func _reload_script(path: String) -> void:
# First, trigger a filesystem scan so Godot knows the file changed
EditorInterface.get_resource_filesystem().scan()
# If the script is already loaded in memory, reload it
if ResourceLoader.exists(path):
var script = load(path)
if script is Script:
script.reload(true)
# If the script is open in the script editor, the reload above updates it.
# But we also need to notify the editor to refresh its error indicators.
EditorInterface.get_script_editor().notification(Control.NOTIFICATION_VISIBILITY_CHANGED)
func _attach_script(params: Dictionary) -> Dictionary:
var result := require_string(params, "node_path")
if result[1] != null:
return result[1]
var node_path: String = result[0]
var result2 := require_string(params, "script_path")
if result2[1] != null:
return result2[1]
var script_path: String = result2[0]
var root := get_edited_root()
if root == null:
return error_no_scene()
var node := find_node_by_path(node_path)
if node == null:
return error_not_found("Node '%s'" % node_path, "Use get_scene_tree to see available nodes")
if not FileAccess.file_exists(script_path):
return error_not_found("Script '%s'" % script_path)
var script: Script = load(script_path)
if script == null:
return error_internal("Failed to load script: %s" % script_path)
var old_script: Variant = node.get_script()
var undo_redo := get_undo_redo()
undo_redo.create_action("MCP: Attach script to %s" % node.name)
undo_redo.add_do_method(node, "set_script", script)
undo_redo.add_undo_method(node, "set_script", old_script)
undo_redo.commit_action()
return success({
"node_path": str(root.get_path_to(node)),
"script_path": script_path,
"attached": true,
})
func _validate_script(params: Dictionary) -> Dictionary:
var result := require_string(params, "path")
if result[1] != null:
return result[1]
var path: String = result[0]
var path_guard := _guard_script_file_path(path, "validate_script")
if not path_guard.is_empty():
return path_guard
if not FileAccess.file_exists(path):
return error_not_found("Script '%s'" % path)
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return error_internal("Cannot read script: %s" % error_string(FileAccess.get_open_error()))
var source_code := file.get_as_text()
file.close()
var script := GDScript.new()
script.source_code = source_code
var err := script.reload()
if err == OK:
return success({"path": path, "valid": true, "message": "Script compiles successfully"})
return success({
"path": path,
"valid": false,
"error_code": err,
"error_string": error_string(err),
"message": "Compilation failed. Use get_output_log or get_editor_errors for details.",
})
func _get_open_scripts(_params: Dictionary) -> Dictionary:
var script_editor := EditorInterface.get_script_editor()
var open_scripts: Array = []
for script_base in script_editor.get_open_scripts():
var info := {
"path": script_base.resource_path,
"type": script_base.get_class(),
}
open_scripts.append(info)
return success({"scripts": open_scripts, "count": open_scripts.size()})