-
Notifications
You must be signed in to change notification settings - Fork 38
Add node_rename, complex node_set_property values, and script_patch #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,6 +80,66 @@ func read_script(params: Dictionary) -> Dictionary: | |
| } | ||
|
|
||
|
|
||
| func patch_script(params: Dictionary) -> Dictionary: | ||
| var path: String = params.get("path", "") | ||
| var old_text: String = params.get("old_text", "") | ||
| var new_text: String = params.get("new_text", "") | ||
| var replace_all: bool = params.get("replace_all", false) | ||
|
|
||
| if path.is_empty(): | ||
| return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Missing required param: path") | ||
| if not path.begins_with("res://"): | ||
| return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Path must start with res://") | ||
| if not "new_text" in params: | ||
| return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Missing required param: new_text") | ||
| if old_text.is_empty(): | ||
| return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "old_text must not be empty") | ||
|
|
||
|
Comment on lines
+89
to
+101
|
||
| var read := FileAccess.open(path, FileAccess.READ) | ||
| if read == null: | ||
| return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "File not found or unreadable: %s" % path) | ||
| var content := read.get_as_text() | ||
| read.close() | ||
|
|
||
| var match_count := content.count(old_text) | ||
| if match_count == 0: | ||
| return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "old_text not found in %s" % path) | ||
| if match_count > 1 and not replace_all: | ||
| return McpErrorCodes.make( | ||
| McpErrorCodes.INVALID_PARAMS, | ||
| "old_text matches %d times; pass replace_all=true or provide a more specific snippet" % match_count, | ||
| ) | ||
|
|
||
| var new_content: String | ||
| var replacements: int | ||
| if replace_all: | ||
| new_content = content.replace(old_text, new_text) | ||
| replacements = match_count | ||
| else: | ||
| var idx := content.find(old_text) | ||
| new_content = content.substr(0, idx) + new_text + content.substr(idx + old_text.length()) | ||
| replacements = 1 | ||
|
|
||
| var write := FileAccess.open(path, FileAccess.WRITE) | ||
| if write == null: | ||
| return McpErrorCodes.make(McpErrorCodes.INTERNAL_ERROR, "Failed to open file for writing: %s" % path) | ||
| write.store_string(new_content) | ||
| write.close() | ||
|
|
||
| EditorInterface.get_resource_filesystem().scan() | ||
|
|
||
| return { | ||
| "data": { | ||
| "path": path, | ||
| "replacements": replacements, | ||
| "size": new_content.length(), | ||
| "old_size": content.length(), | ||
| "undoable": false, | ||
| "reason": "File system operations cannot be undone via editor undo", | ||
| } | ||
| } | ||
|
|
||
|
|
||
| func attach_script(params: Dictionary) -> Dictionary: | ||
| var node_path: String = params.get("path", "") | ||
| var script_path: String = params.get("script_path", "") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,24 +141,54 @@ async def node_set_property( | |
| ctx: Context, | ||
| path: str, | ||
| property: str, | ||
| value: str | int | float | bool | dict | list, | ||
| value: str | int | float | bool | dict | list | None, | ||
| ) -> dict: | ||
| """Set a property on a node. | ||
|
|
||
| Sets a simple property value. For Vector2/Vector3, pass a dict | ||
| with x/y/z keys. For Color, pass a dict with r/g/b/a keys or | ||
| a hex string like "#ff0000". | ||
| Coerces `value` to match the property's declared type: | ||
|
|
||
| - Vector2/Vector3: dict with x/y/z keys | ||
| - Color: dict with r/g/b/a keys, or hex string ("#ff0000") | ||
| - NodePath: string ("../Other/Node") | ||
| - Resource: res:// path string (loads and assigns), or null to clear | ||
| - StringName: plain string | ||
| - Array/Dictionary: pass a JSON list/object | ||
| - bool/int/float: JSON primitives | ||
|
|
||
| Args: | ||
| path: Scene path of the node (e.g. "/Main/Camera3D"). | ||
| property: Property name (e.g. "fov", "position", "visible"). | ||
| value: New value for the property. | ||
| property: Property name (e.g. "fov", "position", "visible", "mesh", "remote_path"). | ||
| value: New value for the property. Pass null to clear a Resource/NodePath. | ||
|
||
| """ | ||
| runtime = DirectRuntime.from_context(ctx) | ||
| return await node_handlers.node_set_property( | ||
| runtime, path=path, property=property, value=value, | ||
| ) | ||
|
|
||
| @mcp.tool(meta=DEFER_META) | ||
| async def node_rename( | ||
| ctx: Context, | ||
| path: str, | ||
| new_name: str, | ||
| ) -> dict: | ||
| """Rename a node in the scene tree. | ||
|
|
||
| Changes the node's `name`. Fails if a sibling already has that name, | ||
| or if the name contains `/`, `:`, or `@`. Cannot rename the scene root. | ||
|
|
||
| Note: `NodePath` properties on OTHER nodes that pointed at this node | ||
| (e.g. a camera's `remote_path`) will not be auto-updated. Scripts that | ||
| reference this node by name (`$OldName`, `get_node("OldName")`) also | ||
| need manual fixes. Children of the renamed node keep working because | ||
| their paths are relative. | ||
|
|
||
| Args: | ||
| path: Scene path of the node to rename (e.g. "/Main/Player"). | ||
| new_name: New name for the node (e.g. "Hero"). | ||
| """ | ||
| runtime = DirectRuntime.from_context(ctx) | ||
| return await node_handlers.node_rename(runtime, path=path, new_name=new_name) | ||
|
|
||
| @mcp.tool(meta=DEFER_META) | ||
| async def node_duplicate( | ||
| ctx: Context, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
patch_scriptchecks for missingnew_textbut not missingold_text. If the caller omitsold_text, the handler returnsold_text must not be emptyrather than a consistentMissing required param: old_text, and it also prevents explicitly passing an empty string (even if that might be useful for no-op validation). Consider adding an explicit"old_text" in paramscheck (mirroringnew_text) and keep the non-empty constraint as a separate validation.