Skip to main content
Glama
godot_operations.gd24.5 kB
@tool extends SceneTree ## Godot Operations Script for MCP Server ## This script is executed in headless mode to perform operations requested by the MCP server. ## Communication happens via command line arguments and stdout markers. func _init(): # Parse command line arguments var args = OS.get_cmdline_user_args() if args.size() < 1: _output_error("No operation specified") quit(1) return var operation = args[0] var params = {} if args.size() > 1: var json = JSON.new() var parse_result = json.parse(args[1]) if parse_result == OK: params = json.data else: _output_error("Failed to parse parameters: " + json.get_error_message()) quit(1) return # Execute the operation var result = _execute_operation(operation, params) _output_result(result) quit(0) func _execute_operation(operation: String, params: Dictionary) -> Dictionary: match operation: "create_scene": return _create_scene(params) "add_node": return _add_node(params) "remove_node": return _remove_node(params) "modify_node": return _modify_node(params) "read_scene": return _read_scene(params) "list_nodes": return _list_nodes(params) "create_script": return _create_script(params) "attach_script": return _attach_script(params) "create_animation": return _create_animation(params) "add_animation_track": return _add_animation_track(params) "create_resource": return _create_resource(params) "get_project_info": return _get_project_info(params) "list_scenes": return _list_scenes(params) "list_scripts": return _list_scripts(params) _: return {"success": false, "error": "Unknown operation: " + operation} # ============== Scene Operations ============== func _create_scene(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") var root_type = params.get("root_type", "Node2D") var root_name = params.get("root_name", "Root") if scene_path.is_empty(): return {"success": false, "error": "scene_path is required"} # Create the root node var root_node = _create_node_of_type(root_type) if root_node == null: return {"success": false, "error": "Unknown node type: " + root_type} root_node.name = root_name # Create a packed scene var packed_scene = PackedScene.new() var result = packed_scene.pack(root_node) if result != OK: root_node.queue_free() return {"success": false, "error": "Failed to pack scene"} # Ensure directory exists var dir_path = scene_path.get_base_dir() if not dir_path.is_empty(): DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(dir_path)) # Save the scene result = ResourceSaver.save(packed_scene, scene_path) root_node.queue_free() if result != OK: return {"success": false, "error": "Failed to save scene: " + str(result)} return { "success": true, "message": "Created scene at " + scene_path, "scene_path": scene_path, "root_type": root_type, "root_name": root_name } func _add_node(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") var parent_path = params.get("parent_path", ".") var node_type = params.get("node_type", "Node") var node_name = params.get("node_name", "NewNode") var properties = params.get("properties", {}) if scene_path.is_empty(): return {"success": false, "error": "scene_path is required"} # Load the scene var packed_scene = load(scene_path) as PackedScene if packed_scene == null: return {"success": false, "error": "Failed to load scene: " + scene_path} var scene_root = packed_scene.instantiate() # Find the parent node var parent_node = scene_root if parent_path == "." else scene_root.get_node_or_null(parent_path) if parent_node == null: scene_root.queue_free() return {"success": false, "error": "Parent node not found: " + parent_path} # Create the new node var new_node = _create_node_of_type(node_type) if new_node == null: scene_root.queue_free() return {"success": false, "error": "Unknown node type: " + node_type} new_node.name = node_name # Set properties for prop_name in properties: if new_node.get(prop_name) != null or prop_name in new_node.get_property_list().map(func(p): return p.name): new_node.set(prop_name, properties[prop_name]) # Add to parent parent_node.add_child(new_node) new_node.owner = scene_root # Save the scene var new_packed_scene = PackedScene.new() new_packed_scene.pack(scene_root) var result = ResourceSaver.save(new_packed_scene, scene_path) scene_root.queue_free() if result != OK: return {"success": false, "error": "Failed to save scene"} return { "success": true, "message": "Added " + node_type + " node '" + node_name + "' to " + parent_path, "node_path": parent_path + "/" + node_name if parent_path != "." else node_name } func _remove_node(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") var node_path = params.get("node_path", "") if scene_path.is_empty() or node_path.is_empty(): return {"success": false, "error": "scene_path and node_path are required"} var packed_scene = load(scene_path) as PackedScene if packed_scene == null: return {"success": false, "error": "Failed to load scene: " + scene_path} var scene_root = packed_scene.instantiate() var node = scene_root.get_node_or_null(node_path) if node == null: scene_root.queue_free() return {"success": false, "error": "Node not found: " + node_path} if node == scene_root: scene_root.queue_free() return {"success": false, "error": "Cannot remove root node"} node.get_parent().remove_child(node) node.queue_free() var new_packed_scene = PackedScene.new() new_packed_scene.pack(scene_root) var result = ResourceSaver.save(new_packed_scene, scene_path) scene_root.queue_free() if result != OK: return {"success": false, "error": "Failed to save scene"} return {"success": true, "message": "Removed node: " + node_path} func _modify_node(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") var node_path = params.get("node_path", "") var properties = params.get("properties", {}) if scene_path.is_empty() or node_path.is_empty(): return {"success": false, "error": "scene_path and node_path are required"} var packed_scene = load(scene_path) as PackedScene if packed_scene == null: return {"success": false, "error": "Failed to load scene: " + scene_path} var scene_root = packed_scene.instantiate() var node = scene_root.get_node_or_null(node_path) if node_path != "." else scene_root if node == null: scene_root.queue_free() return {"success": false, "error": "Node not found: " + node_path} var modified_props = [] for prop_name in properties: var value = properties[prop_name] # Convert special types value = _convert_property_value(value) node.set(prop_name, value) modified_props.append(prop_name) var new_packed_scene = PackedScene.new() new_packed_scene.pack(scene_root) var result = ResourceSaver.save(new_packed_scene, scene_path) scene_root.queue_free() if result != OK: return {"success": false, "error": "Failed to save scene"} return { "success": true, "message": "Modified properties on " + node_path, "modified_properties": modified_props } func _read_scene(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") if scene_path.is_empty(): return {"success": false, "error": "scene_path is required"} var packed_scene = load(scene_path) as PackedScene if packed_scene == null: return {"success": false, "error": "Failed to load scene: " + scene_path} var scene_root = packed_scene.instantiate() var tree_structure = _serialize_node_tree(scene_root) scene_root.queue_free() return { "success": true, "scene_path": scene_path, "tree": tree_structure } func _list_nodes(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") if scene_path.is_empty(): return {"success": false, "error": "scene_path is required"} var packed_scene = load(scene_path) as PackedScene if packed_scene == null: return {"success": false, "error": "Failed to load scene: " + scene_path} var scene_root = packed_scene.instantiate() var nodes = [] _collect_node_paths(scene_root, scene_root, nodes) scene_root.queue_free() return {"success": true, "nodes": nodes} # ============== Script Operations ============== func _create_script(params: Dictionary) -> Dictionary: var script_path = params.get("script_path", "") var extends_type = params.get("extends", "Node") var class_name_str = params.get("class_name", "") var content = params.get("content", "") var template = params.get("template", "default") if script_path.is_empty(): return {"success": false, "error": "script_path is required"} var script_content = "" if not content.is_empty(): script_content = content else: # Generate from template script_content = _generate_script_template(extends_type, class_name_str, template) # Ensure directory exists var dir_path = script_path.get_base_dir() if not dir_path.is_empty(): DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(dir_path)) # Write the script file var file = FileAccess.open(script_path, FileAccess.WRITE) if file == null: return {"success": false, "error": "Failed to create script file"} file.store_string(script_content) file.close() return { "success": true, "message": "Created script at " + script_path, "script_path": script_path } func _attach_script(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") var node_path = params.get("node_path", ".") var script_path = params.get("script_path", "") if scene_path.is_empty() or script_path.is_empty(): return {"success": false, "error": "scene_path and script_path are required"} var packed_scene = load(scene_path) as PackedScene if packed_scene == null: return {"success": false, "error": "Failed to load scene: " + scene_path} var script = load(script_path) as GDScript if script == null: return {"success": false, "error": "Failed to load script: " + script_path} var scene_root = packed_scene.instantiate() var node = scene_root.get_node_or_null(node_path) if node_path != "." else scene_root if node == null: scene_root.queue_free() return {"success": false, "error": "Node not found: " + node_path} node.set_script(script) var new_packed_scene = PackedScene.new() new_packed_scene.pack(scene_root) var result = ResourceSaver.save(new_packed_scene, scene_path) scene_root.queue_free() if result != OK: return {"success": false, "error": "Failed to save scene"} return { "success": true, "message": "Attached script " + script_path + " to " + node_path } # ============== Animation Operations ============== func _create_animation(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") var node_path = params.get("node_path", ".") var animation_name = params.get("animation_name", "default") var duration = params.get("duration", 1.0) var loop = params.get("loop", false) if scene_path.is_empty(): return {"success": false, "error": "scene_path is required"} var packed_scene = load(scene_path) as PackedScene if packed_scene == null: return {"success": false, "error": "Failed to load scene: " + scene_path} var scene_root = packed_scene.instantiate() var target_node = scene_root.get_node_or_null(node_path) if node_path != "." else scene_root if target_node == null: scene_root.queue_free() return {"success": false, "error": "Node not found: " + node_path} # Find or create AnimationPlayer var anim_player: AnimationPlayer = null for child in target_node.get_children(): if child is AnimationPlayer: anim_player = child break if anim_player == null: anim_player = AnimationPlayer.new() anim_player.name = "AnimationPlayer" target_node.add_child(anim_player) anim_player.owner = scene_root # Create the animation var animation = Animation.new() animation.length = duration animation.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE # Add to animation library var library: AnimationLibrary if anim_player.has_animation_library(""): library = anim_player.get_animation_library("") else: library = AnimationLibrary.new() anim_player.add_animation_library("", library) library.add_animation(animation_name, animation) var new_packed_scene = PackedScene.new() new_packed_scene.pack(scene_root) var result = ResourceSaver.save(new_packed_scene, scene_path) scene_root.queue_free() if result != OK: return {"success": false, "error": "Failed to save scene"} return { "success": true, "message": "Created animation '" + animation_name + "' with duration " + str(duration) + "s" } func _add_animation_track(params: Dictionary) -> Dictionary: var scene_path = params.get("scene_path", "") var animation_player_path = params.get("animation_player_path", "") var animation_name = params.get("animation_name", "default") var target_node_path = params.get("target_node_path", "") var property = params.get("property", "") var keyframes = params.get("keyframes", []) if scene_path.is_empty() or animation_player_path.is_empty(): return {"success": false, "error": "scene_path and animation_player_path are required"} var packed_scene = load(scene_path) as PackedScene if packed_scene == null: return {"success": false, "error": "Failed to load scene: " + scene_path} var scene_root = packed_scene.instantiate() var anim_player = scene_root.get_node_or_null(animation_player_path) as AnimationPlayer if anim_player == null: scene_root.queue_free() return {"success": false, "error": "AnimationPlayer not found: " + animation_player_path} var animation = anim_player.get_animation(animation_name) if animation == null: scene_root.queue_free() return {"success": false, "error": "Animation not found: " + animation_name} # Create the track var track_path = target_node_path + ":" + property var track_idx = animation.add_track(Animation.TYPE_VALUE) animation.track_set_path(track_idx, track_path) # Add keyframes for kf in keyframes: var time = kf.get("time", 0.0) var value = _convert_property_value(kf.get("value")) animation.track_insert_key(track_idx, time, value) var new_packed_scene = PackedScene.new() new_packed_scene.pack(scene_root) var result = ResourceSaver.save(new_packed_scene, scene_path) scene_root.queue_free() if result != OK: return {"success": false, "error": "Failed to save scene"} return { "success": true, "message": "Added animation track for " + property + " on " + target_node_path } # ============== Resource Operations ============== func _create_resource(params: Dictionary) -> Dictionary: var resource_path = params.get("resource_path", "") var resource_type = params.get("resource_type", "") var properties = params.get("properties", {}) if resource_path.is_empty() or resource_type.is_empty(): return {"success": false, "error": "resource_path and resource_type are required"} var resource = _create_resource_of_type(resource_type) if resource == null: return {"success": false, "error": "Unknown resource type: " + resource_type} # Set properties for prop_name in properties: var value = _convert_property_value(properties[prop_name]) resource.set(prop_name, value) # Ensure directory exists var dir_path = resource_path.get_base_dir() if not dir_path.is_empty(): DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(dir_path)) var result = ResourceSaver.save(resource, resource_path) if result != OK: return {"success": false, "error": "Failed to save resource"} return { "success": true, "message": "Created " + resource_type + " at " + resource_path } # ============== Project Operations ============== func _get_project_info(params: Dictionary) -> Dictionary: var config = ConfigFile.new() var err = config.load("res://project.godot") if err != OK: return {"success": false, "error": "Failed to load project.godot"} return { "success": true, "project_name": config.get_value("application", "config/name", "Unknown"), "main_scene": config.get_value("application", "run/main_scene", ""), "godot_version": Engine.get_version_info(), "project_path": ProjectSettings.globalize_path("res://") } func _list_scenes(params: Dictionary) -> Dictionary: var scenes = [] _scan_for_files("res://", [".tscn", ".scn"], scenes) return {"success": true, "scenes": scenes} func _list_scripts(params: Dictionary) -> Dictionary: var scripts = [] _scan_for_files("res://", [".gd"], scripts) return {"success": true, "scripts": scripts} # ============== Helper Functions ============== func _create_node_of_type(type_name: String) -> Node: match type_name: "Node": return Node.new() "Node2D": return Node2D.new() "Node3D": return Node3D.new() "Sprite2D": return Sprite2D.new() "Sprite3D": return Sprite3D.new() "Camera2D": return Camera2D.new() "Camera3D": return Camera3D.new() "CharacterBody2D": return CharacterBody2D.new() "CharacterBody3D": return CharacterBody3D.new() "RigidBody2D": return RigidBody2D.new() "RigidBody3D": return RigidBody3D.new() "StaticBody2D": return StaticBody2D.new() "StaticBody3D": return StaticBody3D.new() "Area2D": return Area2D.new() "Area3D": return Area3D.new() "CollisionShape2D": return CollisionShape2D.new() "CollisionShape3D": return CollisionShape3D.new() "MeshInstance3D": return MeshInstance3D.new() "DirectionalLight3D": return DirectionalLight3D.new() "PointLight2D": return PointLight2D.new() "OmniLight3D": return OmniLight3D.new() "SpotLight3D": return SpotLight3D.new() "AnimationPlayer": return AnimationPlayer.new() "AnimatedSprite2D": return AnimatedSprite2D.new() "AudioStreamPlayer": return AudioStreamPlayer.new() "AudioStreamPlayer2D": return AudioStreamPlayer2D.new() "AudioStreamPlayer3D": return AudioStreamPlayer3D.new() "Control": return Control.new() "Label": return Label.new() "Button": return Button.new() "TextureRect": return TextureRect.new() "Panel": return Panel.new() "CanvasLayer": return CanvasLayer.new() "ParallaxBackground": return ParallaxBackground.new() "ParallaxLayer": return ParallaxLayer.new() "TileMap": return TileMap.new() "Path2D": return Path2D.new() "Path3D": return Path3D.new() "PathFollow2D": return PathFollow2D.new() "PathFollow3D": return PathFollow3D.new() "Timer": return Timer.new() "GPUParticles2D": return GPUParticles2D.new() "GPUParticles3D": return GPUParticles3D.new() "CPUParticles2D": return CPUParticles2D.new() "CPUParticles3D": return CPUParticles3D.new() "WorldEnvironment": return WorldEnvironment.new() "NavigationRegion2D": return NavigationRegion2D.new() "NavigationRegion3D": return NavigationRegion3D.new() _: # Try to instantiate by class name if ClassDB.class_exists(type_name): return ClassDB.instantiate(type_name) return null func _create_resource_of_type(type_name: String) -> Resource: match type_name: "CircleShape2D": return CircleShape2D.new() "RectangleShape2D": return RectangleShape2D.new() "CapsuleShape2D": return CapsuleShape2D.new() "BoxShape3D": return BoxShape3D.new() "SphereShape3D": return SphereShape3D.new() "CapsuleShape3D": return CapsuleShape3D.new() "StandardMaterial3D": return StandardMaterial3D.new() "ShaderMaterial": return ShaderMaterial.new() "CanvasItemMaterial": return CanvasItemMaterial.new() "BoxMesh": return BoxMesh.new() "SphereMesh": return SphereMesh.new() "CapsuleMesh": return CapsuleMesh.new() "CylinderMesh": return CylinderMesh.new() "PlaneMesh": return PlaneMesh.new() "QuadMesh": return QuadMesh.new() "Environment": return Environment.new() "Curve": return Curve.new() "Curve2D": return Curve2D.new() "Curve3D": return Curve3D.new() "Gradient": return Gradient.new() "GradientTexture1D": return GradientTexture1D.new() "GradientTexture2D": return GradientTexture2D.new() "NoiseTexture2D": return NoiseTexture2D.new() "Animation": return Animation.new() "AnimationLibrary": return AnimationLibrary.new() "SpriteFrames": return SpriteFrames.new() "LabelSettings": return LabelSettings.new() "StyleBoxFlat": return StyleBoxFlat.new() "StyleBoxTexture": return StyleBoxTexture.new() _: if ClassDB.class_exists(type_name): var res = ClassDB.instantiate(type_name) if res is Resource: return res return null func _generate_script_template(extends_type: String, class_name_str: String, template: String) -> String: var script = "" if not class_name_str.is_empty(): script += "class_name " + class_name_str + "\n" script += "extends " + extends_type + "\n\n" match template: "empty": script += "# Empty script\npass\n" "character_2d": script += """const SPEED = 300.0 const JUMP_VELOCITY = -400.0 func _physics_process(delta: float) -> void: # Add gravity if not is_on_floor(): velocity += get_gravity() * delta # Handle jump if Input.is_action_just_pressed("ui_accept") and is_on_floor(): velocity.y = JUMP_VELOCITY # Get input direction var direction := Input.get_axis("ui_left", "ui_right") if direction: velocity.x = direction * SPEED else: velocity.x = move_toward(velocity.x, 0, SPEED) move_and_slide() """ "character_3d": script += """const SPEED = 5.0 const JUMP_VELOCITY = 4.5 func _physics_process(delta: float) -> void: # Add gravity if not is_on_floor(): velocity += get_gravity() * delta # Handle jump if Input.is_action_just_pressed("ui_accept") and is_on_floor(): velocity.y = JUMP_VELOCITY # Get input direction var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() if direction: velocity.x = direction.x * SPEED velocity.z = direction.z * SPEED else: velocity.x = move_toward(velocity.x, 0, SPEED) velocity.z = move_toward(velocity.z, 0, SPEED) move_and_slide() """ _: # "default" script += """func _ready() -> void: pass func _process(delta: float) -> void: pass """ return script func _serialize_node_tree(node: Node, depth: int = 0) -> Dictionary: var data = { "name": node.name, "type": node.get_class(), "path": str(node.get_path()), } # Include script path if attached if node.get_script(): data["script"] = node.get_script().resource_path # Include children var children = [] for child in node.get_children(): children.append(_serialize_node_tree(child, depth + 1)) if children.size() > 0: data["children"] = children return data func _collect_node_paths(node: Node, root: Node, paths: Array) -> void: var node_path = str(root.get_path_to(node)) if node_path.is_empty() or node_path == ".": node_path = "." paths.append({"path": node_path, "type": node.get_class(), "name": node.name}) for child in node.get_children(): _collect_node_paths(child, root, paths) func _convert_property_value(value): if value is Dictionary: if value.has("_type"): match value["_type"]: "Vector2": return Vector2(value.get("x", 0), value.get("y", 0)) "Vector3": return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0)) "Color": return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1)) "Rect2": return Rect2(value.get("x", 0), value.get("y", 0), value.get("w", 0), value.get("h", 0)) return value func _scan_for_files(path: String, extensions: Array, results: Array) -> void: var dir = DirAccess.open(path) if dir == null: return dir.list_dir_begin() var file_name = dir.get_next() while file_name != "": 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 file_name != "addons": _scan_for_files(full_path, extensions, results) else: for ext in extensions: if file_name.ends_with(ext): results.append(full_path) break file_name = dir.get_next() dir.list_dir_end() func _output_result(result: Dictionary) -> void: print("[GODOT_MCP_RESULT]") print(JSON.stringify(result)) print("[/GODOT_MCP_RESULT]") func _output_error(message: String) -> void: _output_result({"success": false, "error": message})

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/koltyakov/godot-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server