blenderwright 2.1.0__tar.gz → 2.2.0__tar.gz
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.
- {blenderwright-2.1.0 → blenderwright-2.2.0}/PKG-INFO +4 -3
- {blenderwright-2.1.0 → blenderwright-2.2.0}/README.md +3 -2
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/__init__.py +1 -1
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/blender_manifest.toml +1 -1
- blenderwright-2.2.0/addon/handlers/code_exec.py +176 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/ui_panel.py +34 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/index.html +25 -9
- {blenderwright-2.1.0 → blenderwright-2.2.0}/pyproject.toml +1 -1
- blenderwright-2.2.0/src/blenderwright/tools/code_exec.py +39 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_code_exec_handler.py +99 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_ui_panel.py +20 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_code_exec.py +16 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/uv.lock +1 -1
- blenderwright-2.1.0/addon/handlers/code_exec.py +0 -101
- blenderwright-2.1.0/src/blenderwright/tools/code_exec.py +0 -31
- {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/FUNDING.yml +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/workflows/ci.yml +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/workflows/pylint.yml +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/workflows/release.yml +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/.github/workflows/test-bpy51.yml +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/.gitignore +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/LICENSE.md +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/dispatcher.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/animation.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/armature.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/booltool.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/camera.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/collections.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/curves.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/file_ops.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/geometry_nodes.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/gpencil.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/lighting.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/materials.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/mesh_editing.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/mesh_quality.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/modeling.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/objects.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/physics.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/print3d.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/procedural.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/raster.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/rendering.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/scene.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/sculpting.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/selection.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/sequencer.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/sweep.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/transforms.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/uv.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/handlers/viewport.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/render_guard.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/server.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/addon/thread_safety.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/blend-ai-v1.0.0.zip +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/build.ps1 +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/build.sh +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/.nojekyll +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/CNAME +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/googled2f6c71abc2d6a7b.html +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/robots.txt +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/shuttle-launch.mp4 +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/docs/sitemap.xml +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/glama.json +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/install-addon.sh +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/install_addon.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/launch_ollama.sh +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/saloon.png +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/screenshot.png +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/scripts/generate_tool_docs.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/shader-preview.png +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/shuttle-launch.gif +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/shuttle-launch.png +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/__main__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/aliases.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/connection.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/enum_hints.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/ollama_chat.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/prompts/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/prompts/workflows.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/resources/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/resources/scene_info.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/server.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/strict.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tool_registry.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/animation.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/armature.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/booltool.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/camera.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/collections.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/curves.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/file_ops.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/geometry_nodes.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/gpencil.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/lighting.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/materials.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/mesh_editing.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/mesh_quality.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/modeling.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/objects.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/physics.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/print3d.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/rendering.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/scene.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/screenshot.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/sculpting.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/selection.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/sequencer.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/sweep.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/transforms.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/uv.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/tools/viewport.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/toolsets.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/src/blenderwright/validators.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/conftest.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/conftest.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_dispatcher.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handler_registration.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_camera_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_geometry_nodes_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_gpencil_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_materials_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_mesh_quality_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_modeling_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_objects_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_print3d_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_procedural_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_raster_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_rendering_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_scene_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sculpting_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_selection_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sequencer_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sweep_handler.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_layer_consistency.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_module_cache_purge.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_render_guard.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_server.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_thread_safety.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_command_registry.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_connection.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_enum_hints.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_installer.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_long_names.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_object_param_naming.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_ollama_budget.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_ollama_chat.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_ollama_toolsets.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_ollama_vision.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_project_metadata.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_round_wasters.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_strict_parameters.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tool_disambiguation.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/__init__.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/conftest.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_animation.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_armature.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_auto_critique_prompt.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_booltool.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_camera.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_collections.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_curves.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_eevee_light_path.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_extensions.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_file_ops.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_geometry_nodes.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_gpencil.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_issue_31_validation.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_lighting.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_materials.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_mesh_editing.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_mesh_quality.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_mesh_repair.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_modeling.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_negative_scale_primitives.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_new_nodes.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_numeric_text_values.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_objects.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_physics.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_point_camera_at.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_print3d.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_procedural.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_prompts.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_raster.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_rendering.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_scene.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_screenshot.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_sculpting.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_selection.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_selection_respected.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_sequencer.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_shader_nodes.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_sweep.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_transforms.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_uv.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_uv_slim.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_tools/test_viewport.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_toolsets.py +0 -0
- {blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blenderwright
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: The most intuitive and efficient MCP Server for Blender
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -49,7 +49,7 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
|
|
|
49
49
|
- **Visual feedback loop**: fast viewport screenshots via OpenGL render (~ms, not seconds) with auto-critique prompts that guide the LLM to check its own work
|
|
50
50
|
- **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
|
|
51
51
|
- **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
|
|
52
|
-
- **
|
|
52
|
+
- **Raw Python is opt-in**: `execute_blender_code` is off until you tick "Allow raw Python" in the N-panel, per session, never saved into the .blend. When on, every snippet is echoed to the console and listed in the panel. A restricted namespace blocks obvious mistakes (`os`, `subprocess`, `exec`, `open`, ...); it is a guard against accidents, not a security boundary
|
|
53
53
|
- **Render-aware**: automatically detects when Blender is rendering and queues commands. Recovers from stuck render guards via `load_post` handler and reset command
|
|
54
54
|
- **Blender 4.2+ compatible**: ships as a Blender Extension; tested against Blender 5.1 with EEVEE identifier, Annotation API, sculpt stroke_method, SLIM UV unwrap, Raycast shader node, and EEVEE light path intensity controls
|
|
55
55
|
- **Custom port**: configure the server port from the N-panel UI (default: 9876, range: 1024–65535)
|
|
@@ -349,7 +349,8 @@ AI Assistant <--stdio/MCP--> blenderwright server <--TCP socket--> Blender addon
|
|
|
349
349
|
<summary><strong>Security</strong></summary>
|
|
350
350
|
|
|
351
351
|
- **Localhost only**: The TCP socket binds to `127.0.0.1`, never exposed to the network.
|
|
352
|
-
- **
|
|
352
|
+
- **Raw Python is off by default**: `execute_blender_code` refuses to run until you tick "Allow raw Python" in the blenderwright N-panel. The switch is a `WindowManager` property, so it is per Blender session and is never written into a `.blend` file. Every run is echoed to the system console and the panel shows a count and the last snippet. The 191 structured tools do not need it.
|
|
353
|
+
- **The restricted namespace is not a sandbox**: when raw Python is on, the import hook refuses 25 imports (`os`, `subprocess`, `socket`, `shutil`, `sys`, `ctypes`, `importlib`, `pathlib`, `signal`, `multiprocessing`, `pickle`, `shelve`, `tempfile`, `http`, `urllib`, `ftplib`, `smtplib`, `xmlrpc`, `code`, `codeop`, `compileall`, `webbrowser`, `antigravity`, `turtle`, `tkinter`) and the builtins `__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help` and `memoryview` are removed. That catches a model making a mistake. It does not contain an adversary: the code runs in-process with full `bpy`, and `bpy` alone can save files anywhere and run any operator. Treat "Allow raw Python" the way you would treat running a script someone else wrote: read what the model is about to run, and turn it off when you are done.
|
|
353
354
|
- **Input validation**: All inputs pass through validators before reaching Blender: name sanitization, path traversal prevention, numeric range checks, enum allowlists.
|
|
354
355
|
- **File safety**: Import operations disable `use_scripts_auto_execute` to prevent script injection from imported files. File extensions are checked against allowlists.
|
|
355
356
|
- **Command allowlist**: The addon dispatcher only processes explicitly registered commands. Unknown commands are rejected.
|
|
@@ -31,7 +31,7 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
|
|
|
31
31
|
- **Visual feedback loop**: fast viewport screenshots via OpenGL render (~ms, not seconds) with auto-critique prompts that guide the LLM to check its own work
|
|
32
32
|
- **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
|
|
33
33
|
- **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
|
|
34
|
-
- **
|
|
34
|
+
- **Raw Python is opt-in**: `execute_blender_code` is off until you tick "Allow raw Python" in the N-panel, per session, never saved into the .blend. When on, every snippet is echoed to the console and listed in the panel. A restricted namespace blocks obvious mistakes (`os`, `subprocess`, `exec`, `open`, ...); it is a guard against accidents, not a security boundary
|
|
35
35
|
- **Render-aware**: automatically detects when Blender is rendering and queues commands. Recovers from stuck render guards via `load_post` handler and reset command
|
|
36
36
|
- **Blender 4.2+ compatible**: ships as a Blender Extension; tested against Blender 5.1 with EEVEE identifier, Annotation API, sculpt stroke_method, SLIM UV unwrap, Raycast shader node, and EEVEE light path intensity controls
|
|
37
37
|
- **Custom port**: configure the server port from the N-panel UI (default: 9876, range: 1024–65535)
|
|
@@ -331,7 +331,8 @@ AI Assistant <--stdio/MCP--> blenderwright server <--TCP socket--> Blender addon
|
|
|
331
331
|
<summary><strong>Security</strong></summary>
|
|
332
332
|
|
|
333
333
|
- **Localhost only**: The TCP socket binds to `127.0.0.1`, never exposed to the network.
|
|
334
|
-
- **
|
|
334
|
+
- **Raw Python is off by default**: `execute_blender_code` refuses to run until you tick "Allow raw Python" in the blenderwright N-panel. The switch is a `WindowManager` property, so it is per Blender session and is never written into a `.blend` file. Every run is echoed to the system console and the panel shows a count and the last snippet. The 191 structured tools do not need it.
|
|
335
|
+
- **The restricted namespace is not a sandbox**: when raw Python is on, the import hook refuses 25 imports (`os`, `subprocess`, `socket`, `shutil`, `sys`, `ctypes`, `importlib`, `pathlib`, `signal`, `multiprocessing`, `pickle`, `shelve`, `tempfile`, `http`, `urllib`, `ftplib`, `smtplib`, `xmlrpc`, `code`, `codeop`, `compileall`, `webbrowser`, `antigravity`, `turtle`, `tkinter`) and the builtins `__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help` and `memoryview` are removed. That catches a model making a mistake. It does not contain an adversary: the code runs in-process with full `bpy`, and `bpy` alone can save files anywhere and run any operator. Treat "Allow raw Python" the way you would treat running a script someone else wrote: read what the model is about to run, and turn it off when you are done.
|
|
335
336
|
- **Input validation**: All inputs pass through validators before reaching Blender: name sanitization, path traversal prevention, numeric range checks, enum allowlists.
|
|
336
337
|
- **File safety**: Import operations disable `use_scripts_auto_execute` to prevent script injection from imported files. File extensions are checked against allowlists.
|
|
337
338
|
- **Command allowlist**: The addon dispatcher only processes explicitly registered commands. Unknown commands are rejected.
|
|
@@ -8,7 +8,7 @@ Blender's Python API.
|
|
|
8
8
|
bl_info = {
|
|
9
9
|
"name": "blenderwright",
|
|
10
10
|
"author": "blenderwright",
|
|
11
|
-
"version": (2,
|
|
11
|
+
"version": (2, 2, 0),
|
|
12
12
|
"blender": (4, 2, 0),
|
|
13
13
|
"location": "View3D > Sidebar > blenderwright",
|
|
14
14
|
"description": "MCP Server integration for AI-assisted 3D workflows",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Blender addon handler for raw Python execution (off by default).
|
|
2
|
+
|
|
3
|
+
What this is: a restricted namespace that blocks the obvious footguns
|
|
4
|
+
(import os/subprocess/socket..., exec/eval/open/compile) so a model
|
|
5
|
+
that makes a mistake fails loudly instead of touching the filesystem
|
|
6
|
+
or the network by accident.
|
|
7
|
+
|
|
8
|
+
What this is NOT: a security boundary. The code runs in-process inside
|
|
9
|
+
Blender with full `bpy`, and `bpy` alone can save files anywhere and
|
|
10
|
+
run any operator. A denylist cannot contain a determined adversary in
|
|
11
|
+
the same interpreter, and this module does not claim to.
|
|
12
|
+
|
|
13
|
+
So the real control is the switch, not the list:
|
|
14
|
+
|
|
15
|
+
- `execute_code` is refused unless the user has ticked "Allow raw
|
|
16
|
+
Python" in the blenderwright N-panel. The flag lives on
|
|
17
|
+
`bpy.types.WindowManager`, so it is per session and is never saved
|
|
18
|
+
into a .blend file that could be handed to someone else.
|
|
19
|
+
- Every snippet that runs is echoed to the system console and kept in
|
|
20
|
+
a short log that the panel displays, so the user can see what the
|
|
21
|
+
model actually did.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
import io
|
|
26
|
+
import time
|
|
27
|
+
from collections import deque
|
|
28
|
+
from .. import dispatcher
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Name of the per-session switch registered on bpy.types.WindowManager
|
|
32
|
+
# by ui_panel.register(). Not a Scene property on purpose: Scene props
|
|
33
|
+
# are written into the .blend.
|
|
34
|
+
SWITCH_PROP = "blenderwright_allow_code_exec"
|
|
35
|
+
|
|
36
|
+
# How many recent runs the panel can show.
|
|
37
|
+
EXEC_LOG_SIZE = 20
|
|
38
|
+
_exec_log: deque = deque(maxlen=EXEC_LOG_SIZE)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_enabled() -> bool:
|
|
42
|
+
"""True only if the user turned raw Python on for this Blender session.
|
|
43
|
+
|
|
44
|
+
Anything other than a real `True` (missing property, wrong type, no
|
|
45
|
+
window manager yet, bpy not importable) counts as off.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
import bpy
|
|
49
|
+
flag = getattr(bpy.context.window_manager, SWITCH_PROP, False)
|
|
50
|
+
except Exception:
|
|
51
|
+
return False
|
|
52
|
+
return isinstance(flag, bool) and flag
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_exec_log() -> list:
|
|
56
|
+
"""Copy of the recent-run log, oldest first."""
|
|
57
|
+
return list(_exec_log)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _record(code: str, ok: bool) -> None:
|
|
61
|
+
lines = code.strip().splitlines() or [""]
|
|
62
|
+
_exec_log.append({
|
|
63
|
+
"time": time.strftime("%H:%M:%S"),
|
|
64
|
+
"first_line": lines[0].strip(),
|
|
65
|
+
"lines": len(lines),
|
|
66
|
+
"ok": ok,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Modules refused by the import hook. A guard against accidental misuse by
|
|
71
|
+
# the model, not a security control: see the module docstring.
|
|
72
|
+
BLOCKED_MODULES = frozenset({
|
|
73
|
+
"os", "subprocess", "socket", "shutil", "sys", "ctypes",
|
|
74
|
+
"importlib", "pathlib", "signal", "multiprocessing",
|
|
75
|
+
"pickle", "shelve", "tempfile", "http", "urllib", "ftplib",
|
|
76
|
+
"smtplib", "xmlrpc", "code", "codeop", "compileall",
|
|
77
|
+
"webbrowser", "antigravity", "turtle", "tkinter",
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
# Builtins removed from the restricted namespace. Same caveat as above.
|
|
81
|
+
_REMOVED_BUILTINS = frozenset({
|
|
82
|
+
"__import__", "exec", "eval", "compile", "open",
|
|
83
|
+
"globals", "locals", "vars", "input", "breakpoint",
|
|
84
|
+
"exit", "quit", "help", "memoryview",
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
# Reference to the real __import__ for the safe import hook
|
|
88
|
+
_real_import = builtins_import = __builtins__.__import__ if hasattr(
|
|
89
|
+
__builtins__, "__import__"
|
|
90
|
+
) else __builtins__["__import__"] if isinstance(__builtins__, dict) else __import__
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _safe_import(name, *args, **kwargs):
|
|
94
|
+
"""Import hook that refuses the modules in BLOCKED_MODULES."""
|
|
95
|
+
base_module = name.split(".")[0]
|
|
96
|
+
if base_module in BLOCKED_MODULES:
|
|
97
|
+
raise ImportError(
|
|
98
|
+
f"Module '{name}' is blocked inside execute_blender_code. "
|
|
99
|
+
f"Use the structured MCP tools instead of direct Python imports."
|
|
100
|
+
)
|
|
101
|
+
return _real_import(name, *args, **kwargs)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _build_safe_builtins():
|
|
105
|
+
"""Build the restricted builtins dict."""
|
|
106
|
+
# Get all builtins as a dict
|
|
107
|
+
if isinstance(__builtins__, dict):
|
|
108
|
+
raw = dict(__builtins__)
|
|
109
|
+
else:
|
|
110
|
+
raw = {k: getattr(__builtins__, k) for k in dir(__builtins__)}
|
|
111
|
+
|
|
112
|
+
# Remove dangerous builtins
|
|
113
|
+
safe = {k: v for k, v in raw.items() if k not in _REMOVED_BUILTINS}
|
|
114
|
+
|
|
115
|
+
# Add our safe import hook
|
|
116
|
+
safe["__import__"] = _safe_import
|
|
117
|
+
|
|
118
|
+
return safe
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Build once at module load
|
|
122
|
+
SAFE_BUILTINS = _build_safe_builtins()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def handle_execute_code(params: dict) -> dict:
|
|
126
|
+
"""Run Python inside Blender, if the user has allowed it this session.
|
|
127
|
+
|
|
128
|
+
- Refuses with PermissionError while "Allow raw Python" is unticked.
|
|
129
|
+
- Echoes the snippet to the system console (stderr) and records it
|
|
130
|
+
in the exec log before running it.
|
|
131
|
+
- Runs in the restricted namespace (see module docstring for what
|
|
132
|
+
that does and does not mean) and captures stdout.
|
|
133
|
+
"""
|
|
134
|
+
code = params.get("code", "")
|
|
135
|
+
if not code:
|
|
136
|
+
raise ValueError("No code provided")
|
|
137
|
+
|
|
138
|
+
if not is_enabled():
|
|
139
|
+
raise PermissionError(
|
|
140
|
+
"execute_blender_code is disabled. Raw Python is off by default; "
|
|
141
|
+
"tick 'Allow raw Python' in the blenderwright panel "
|
|
142
|
+
"(View3D > Sidebar > blenderwright) to enable it for this Blender "
|
|
143
|
+
"session, or use the structured tools instead."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Echo to the real console so the user can see what ran, regardless
|
|
147
|
+
# of whether the MCP client shows tool inputs. Goes to stderr so it
|
|
148
|
+
# is never mixed into the captured stdout returned to the model.
|
|
149
|
+
print(
|
|
150
|
+
f"[blenderwright] execute_code ({time.strftime('%H:%M:%S')}):\n{code}",
|
|
151
|
+
file=sys.stderr,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Capture stdout
|
|
155
|
+
old_stdout = sys.stdout
|
|
156
|
+
sys.stdout = buffer = io.StringIO()
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
exec(code, {"__builtins__": SAFE_BUILTINS})
|
|
160
|
+
output = buffer.getvalue()
|
|
161
|
+
_record(code, ok=True)
|
|
162
|
+
return {
|
|
163
|
+
"output": output.strip(),
|
|
164
|
+
"success": True,
|
|
165
|
+
}
|
|
166
|
+
except Exception as e:
|
|
167
|
+
output = buffer.getvalue()
|
|
168
|
+
_record(code, ok=False)
|
|
169
|
+
raise RuntimeError(f"{type(e).__name__}: {e}\nOutput before error: {output}")
|
|
170
|
+
finally:
|
|
171
|
+
sys.stdout = old_stdout
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def register():
|
|
175
|
+
"""Register code execution handler with the dispatcher."""
|
|
176
|
+
dispatcher.register_handler("execute_code", handle_execute_code)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import bpy
|
|
4
4
|
|
|
5
5
|
from . import server as addon_server
|
|
6
|
+
from .handlers import code_exec
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class BLENDERWRIGHT_PT_MainPanel(bpy.types.Panel):
|
|
@@ -26,6 +27,23 @@ class BLENDERWRIGHT_PT_MainPanel(bpy.types.Panel):
|
|
|
26
27
|
layout.prop(context.scene, "blenderwright_port", text="Port")
|
|
27
28
|
layout.operator("blenderwright.start_server", text="Start Server", icon="PLAY")
|
|
28
29
|
|
|
30
|
+
# Raw Python switch: per session, never saved into the .blend.
|
|
31
|
+
box = layout.box()
|
|
32
|
+
wm = context.window_manager
|
|
33
|
+
box.prop(wm, code_exec.SWITCH_PROP, text="Allow raw Python")
|
|
34
|
+
if getattr(wm, code_exec.SWITCH_PROP, False):
|
|
35
|
+
box.label(text="Model may run Python in this session", icon="ERROR")
|
|
36
|
+
log = code_exec.get_exec_log()
|
|
37
|
+
if log:
|
|
38
|
+
last = log[-1]
|
|
39
|
+
status = "ok" if last["ok"] else "failed"
|
|
40
|
+
box.label(text=f"{len(log)} run(s), last {last['time']} ({status}):")
|
|
41
|
+
box.label(text=f" {last['first_line'][:48]}")
|
|
42
|
+
else:
|
|
43
|
+
box.label(text="No runs yet this session")
|
|
44
|
+
else:
|
|
45
|
+
box.label(text="execute_blender_code is off", icon="LOCKED")
|
|
46
|
+
|
|
29
47
|
|
|
30
48
|
class BLENDERWRIGHT_OT_StartServer(bpy.types.Operator):
|
|
31
49
|
"""Start the blenderwright MCP server"""
|
|
@@ -69,10 +87,26 @@ def register():
|
|
|
69
87
|
max=65535,
|
|
70
88
|
)
|
|
71
89
|
|
|
90
|
+
# WindowManager, not Scene: WindowManager properties are not written
|
|
91
|
+
# into the .blend, so the switch cannot travel with a file and is
|
|
92
|
+
# always off when Blender starts.
|
|
93
|
+
setattr(bpy.types.WindowManager, code_exec.SWITCH_PROP, bpy.props.BoolProperty(
|
|
94
|
+
name="Allow raw Python",
|
|
95
|
+
description=(
|
|
96
|
+
"Let execute_blender_code run model-written Python in this Blender "
|
|
97
|
+
"session. Off by default. The restricted namespace blocks obvious "
|
|
98
|
+
"mistakes but is not a security boundary: bpy itself can write files "
|
|
99
|
+
"and run any operator. Every run is echoed to the system console"
|
|
100
|
+
),
|
|
101
|
+
default=False,
|
|
102
|
+
))
|
|
103
|
+
|
|
72
104
|
|
|
73
105
|
def unregister():
|
|
74
106
|
if hasattr(bpy.types.Scene, "blenderwright_port"):
|
|
75
107
|
del bpy.types.Scene.blenderwright_port
|
|
108
|
+
if hasattr(bpy.types.WindowManager, code_exec.SWITCH_PROP):
|
|
109
|
+
delattr(bpy.types.WindowManager, code_exec.SWITCH_PROP)
|
|
76
110
|
|
|
77
111
|
for cls in reversed(classes):
|
|
78
112
|
bpy.utils.unregister_class(cls)
|
|
@@ -261,15 +261,31 @@ boolean modifier (a warning will be included in the response).</p><table class="
|
|
|
261
261
|
<tr><th>location</th><td>XYZ location to point at as [x, y, z]. Mutually exclusive with target.</td></tr></tbody></table><p class="ret"><span>Returns</span> Confirmation dict.</p></article><article class="tool" id="set_active_camera" data-search="set_active_camera set the active scene camera."><h3><a href="#set_active_camera">set_active_camera</a></h3><pre class="sig"><code>set_active_camera(object_name: str)</code></pre><p>Set the active scene camera.</p><table class="args"><tbody><tr><th>object_name</th><td>Name of the camera object to make active.</td></tr></tbody></table><p class="ret"><span>Returns</span> Confirmation dict.</p></article><article class="tool" id="set_camera_from_view" data-search="set_camera_from_view match the active camera to the current 3d viewport view."><h3><a href="#set_camera_from_view">set_camera_from_view</a></h3><pre class="sig"><code>set_camera_from_view()</code></pre><p>Match the active camera to the current 3D viewport view.</p><p class="ret"><span>Returns</span> Confirmation dict with the camera's new location and rotation.</p></article><article class="tool" id="set_camera_property" data-search="set_camera_property set a property on a camera."><h3><a href="#set_camera_property">set_camera_property</a></h3><pre class="sig"><code>set_camera_property(object_name: str, property: str, value: Any)</code></pre><p>Set a property on a camera.</p><table class="args"><tbody><tr><th>object_name</th><td>Name of the camera object.</td></tr>
|
|
262
262
|
<tr><th>property</th><td>Property to set. One of: lens, clip_start, clip_end, sensor_width, sensor_height, dof.use_dof, dof.focus_distance, dof.aperture_fstop, ortho_scale, shift_x, shift_y, type, sensor_fit.</td></tr>
|
|
263
263
|
<tr><th>value</th><td>The value to set.</td></tr></tbody></table><p class="ret"><span>Returns</span> Confirmation dict.</p></article></section>
|
|
264
|
-
<section class="module" id="code_exec" data-module="code execution"><h2>Code Execution <span class="n">1</span></h2><article class="tool" id="execute_blender_code" data-search="execute_blender_code
|
|
265
|
-
|
|
266
|
-
the
|
|
267
|
-
(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
264
|
+
<section class="module" id="code_exec" data-module="code execution"><h2>Code Execution <span class="n">1</span></h2><article class="tool" id="execute_blender_code" data-search="execute_blender_code run python inside blender. off by default; prefer the structured tools.
|
|
265
|
+
|
|
266
|
+
the user must tick "allow raw python" in the blenderwright n-panel
|
|
267
|
+
(view3d > sidebar > blenderwright) for the current blender session.
|
|
268
|
+
until they do, this tool returns an error saying so; relay that to the
|
|
269
|
+
user and do not retry. never ask the user to enable it just to do
|
|
270
|
+
something a structured tool already covers.
|
|
271
|
+
|
|
272
|
+
the code runs in a restricted namespace that blocks obvious mistakes
|
|
273
|
+
(import os/subprocess/socket, exec/eval/open/compile). that is a guard
|
|
274
|
+
against accidents, not a security boundary: the code runs in-process
|
|
275
|
+
with full bpy. every snippet is echoed to blender's system console and
|
|
276
|
+
listed in the panel so the user can see what ran."><h3><a href="#execute_blender_code">execute_blender_code</a></h3><pre class="sig"><code>execute_blender_code(code: str)</code></pre><p>Run Python inside Blender. OFF BY DEFAULT; prefer the structured tools.
|
|
277
|
+
|
|
278
|
+
The user must tick "Allow raw Python" in the blenderwright N-panel
|
|
279
|
+
(View3D > Sidebar > blenderwright) for the current Blender session.
|
|
280
|
+
Until they do, this tool returns an error saying so; relay that to the
|
|
281
|
+
user and do not retry. Never ask the user to enable it just to do
|
|
282
|
+
something a structured tool already covers.
|
|
283
|
+
|
|
284
|
+
The code runs in a restricted namespace that blocks obvious mistakes
|
|
285
|
+
(import os/subprocess/socket, exec/eval/open/compile). That is a guard
|
|
286
|
+
against accidents, not a security boundary: the code runs in-process
|
|
287
|
+
with full bpy. Every snippet is echoed to Blender's system console and
|
|
288
|
+
listed in the panel so the user can see what ran.</p><table class="args"><tbody><tr><th>code</th><td>Python code to execute. bpy is available but must be imported.</td></tr></tbody></table><p class="ret"><span>Returns</span> Dict with 'output' (captured stdout) and 'success' boolean.</p></article></section>
|
|
273
289
|
<section class="module" id="collections" data-module="collections"><h2>Collections <span class="n">4</span></h2><article class="tool" id="create_collection" data-search="create_collection create a new collection, optionally nested under a parent collection."><h3><a href="#create_collection">create_collection</a></h3><pre class="sig"><code>create_collection(name: str, parent: str = '')</code></pre><p>Create a new collection, optionally nested under a parent collection.</p><table class="args"><tbody><tr><th>name</th><td>Name for the new collection.</td></tr>
|
|
274
290
|
<tr><th>parent</th><td>Optional name of parent collection to nest under. Empty string uses the scene's root collection.</td></tr></tbody></table><p class="ret"><span>Returns</span> Dict with the created collection's name and parent.</p></article><article class="tool" id="delete_collection" data-search="delete_collection delete a collection."><h3><a href="#delete_collection">delete_collection</a></h3><pre class="sig"><code>delete_collection(collection_name: str, delete_objects: bool = False)</code></pre><p>Delete a collection.</p><table class="args"><tbody><tr><th>collection_name</th><td>Name of the collection to delete.</td></tr>
|
|
275
291
|
<tr><th>delete_objects</th><td>If True, also delete all objects in the collection. If False, objects are unlinked but kept in the scene.</td></tr></tbody></table><p class="ret"><span>Returns</span> Confirmation dict.</p></article><article class="tool" id="move_to_collection" data-search="move_to_collection move objects to a collection, unlinking them from their current collections."><h3><a href="#move_to_collection">move_to_collection</a></h3><pre class="sig"><code>move_to_collection(object_names: list[str], collection_name: str)</code></pre><p>Move objects to a collection, unlinking them from their current collections.</p><table class="args"><tbody><tr><th>object_names</th><td>List of object names to move.</td></tr>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""MCP tool for running raw Python inside Blender. Off by default."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from blenderwright.server import mcp, get_connection
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@mcp.tool()
|
|
9
|
+
def execute_blender_code(
|
|
10
|
+
code: str,
|
|
11
|
+
) -> dict[str, Any]:
|
|
12
|
+
"""Run Python inside Blender. OFF BY DEFAULT; prefer the structured tools.
|
|
13
|
+
|
|
14
|
+
The user must tick "Allow raw Python" in the blenderwright N-panel
|
|
15
|
+
(View3D > Sidebar > blenderwright) for the current Blender session.
|
|
16
|
+
Until they do, this tool returns an error saying so; relay that to the
|
|
17
|
+
user and do not retry. Never ask the user to enable it just to do
|
|
18
|
+
something a structured tool already covers.
|
|
19
|
+
|
|
20
|
+
The code runs in a restricted namespace that blocks obvious mistakes
|
|
21
|
+
(import os/subprocess/socket, exec/eval/open/compile). That is a guard
|
|
22
|
+
against accidents, not a security boundary: the code runs in-process
|
|
23
|
+
with full bpy. Every snippet is echoed to Blender's system console and
|
|
24
|
+
listed in the panel so the user can see what ran.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
code: Python code to execute. bpy is available but must be imported.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict with 'output' (captured stdout) and 'success' boolean.
|
|
31
|
+
"""
|
|
32
|
+
if not code or not code.strip():
|
|
33
|
+
raise ValueError("No code provided")
|
|
34
|
+
|
|
35
|
+
conn = get_connection()
|
|
36
|
+
response = conn.send_command("execute_code", {"code": code})
|
|
37
|
+
if response.get("status") == "error":
|
|
38
|
+
raise RuntimeError(f"Blender error: {response.get('result')}")
|
|
39
|
+
return response.get("result")
|
{blenderwright-2.1.0 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_code_exec_handler.py
RENAMED
|
@@ -35,6 +35,20 @@ def code_exec_handler():
|
|
|
35
35
|
return mod
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
@pytest.fixture(autouse=True)
|
|
39
|
+
def raw_python_enabled(code_exec_handler):
|
|
40
|
+
"""Turn the per-session switch on for the sandbox tests below.
|
|
41
|
+
|
|
42
|
+
The switch lives on bpy.types.WindowManager so it is never saved into a
|
|
43
|
+
.blend. The mock bpy would otherwise return a truthy MagicMock, so the
|
|
44
|
+
handler must only honour a real bool True; set it explicitly here.
|
|
45
|
+
"""
|
|
46
|
+
import bpy
|
|
47
|
+
bpy.context.window_manager.blenderwright_allow_code_exec = True
|
|
48
|
+
yield
|
|
49
|
+
bpy.context.window_manager.blenderwright_allow_code_exec = False
|
|
50
|
+
|
|
51
|
+
|
|
38
52
|
class TestSandboxBlocksDangerousImports:
|
|
39
53
|
"""Verify that dangerous modules are blocked."""
|
|
40
54
|
|
|
@@ -187,3 +201,88 @@ class TestSandboxConstants:
|
|
|
187
201
|
def test_blocked_modules_is_set(self, code_exec_handler):
|
|
188
202
|
"""BLOCKED_MODULES is a set (for O(1) lookup)."""
|
|
189
203
|
assert isinstance(code_exec_handler.BLOCKED_MODULES, (set, frozenset))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TestRawPythonIsOffByDefault:
|
|
207
|
+
"""execute_code refuses unless the user ticked the box this session."""
|
|
208
|
+
|
|
209
|
+
def test_disabled_when_switch_is_false(self, code_exec_handler):
|
|
210
|
+
import bpy
|
|
211
|
+
bpy.context.window_manager.blenderwright_allow_code_exec = False
|
|
212
|
+
with pytest.raises(PermissionError, match="(?i)disabled|Allow raw Python"):
|
|
213
|
+
code_exec_handler.handle_execute_code({"code": "print('x')"})
|
|
214
|
+
|
|
215
|
+
def test_disabled_when_switch_is_missing(self, code_exec_handler):
|
|
216
|
+
"""No property registered (or a non-bool stand-in) means off."""
|
|
217
|
+
import bpy
|
|
218
|
+
bpy.context.window_manager.blenderwright_allow_code_exec = MagicMock()
|
|
219
|
+
with pytest.raises(PermissionError):
|
|
220
|
+
code_exec_handler.handle_execute_code({"code": "print('x')"})
|
|
221
|
+
|
|
222
|
+
def test_disabled_when_bpy_context_raises(self, code_exec_handler, monkeypatch):
|
|
223
|
+
"""Any failure reading the switch is treated as off, never on."""
|
|
224
|
+
|
|
225
|
+
class _NoContext:
|
|
226
|
+
@property
|
|
227
|
+
def context(self):
|
|
228
|
+
raise AttributeError("no context yet")
|
|
229
|
+
|
|
230
|
+
monkeypatch.setitem(sys.modules, "bpy", _NoContext())
|
|
231
|
+
assert code_exec_handler.is_enabled() is False
|
|
232
|
+
|
|
233
|
+
def test_refusal_does_not_run_code(self, code_exec_handler):
|
|
234
|
+
import bpy
|
|
235
|
+
bpy.context.window_manager.blenderwright_allow_code_exec = False
|
|
236
|
+
code_exec_handler._exec_log.clear()
|
|
237
|
+
before = 0
|
|
238
|
+
with pytest.raises(PermissionError):
|
|
239
|
+
code_exec_handler.handle_execute_code({"code": "print('never')"})
|
|
240
|
+
assert len(code_exec_handler.get_exec_log()) == before
|
|
241
|
+
|
|
242
|
+
def test_refusal_message_names_the_panel(self, code_exec_handler):
|
|
243
|
+
import bpy
|
|
244
|
+
bpy.context.window_manager.blenderwright_allow_code_exec = False
|
|
245
|
+
with pytest.raises(PermissionError, match="blenderwright"):
|
|
246
|
+
code_exec_handler.handle_execute_code({"code": "print('x')"})
|
|
247
|
+
|
|
248
|
+
def test_enabled_runs(self, code_exec_handler):
|
|
249
|
+
result = code_exec_handler.handle_execute_code({"code": "print('on')"})
|
|
250
|
+
assert result["success"] is True
|
|
251
|
+
assert "on" in result["output"]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TestExecLog:
|
|
255
|
+
"""Every run is recorded so the user can see what the model did."""
|
|
256
|
+
|
|
257
|
+
def test_records_each_run(self, code_exec_handler):
|
|
258
|
+
code_exec_handler._exec_log.clear()
|
|
259
|
+
code_exec_handler.handle_execute_code({"code": "print('a')\nprint('b')"})
|
|
260
|
+
log = code_exec_handler.get_exec_log()
|
|
261
|
+
assert len(log) == 1
|
|
262
|
+
entry = log[-1]
|
|
263
|
+
assert entry["first_line"] == "print('a')"
|
|
264
|
+
assert entry["lines"] == 2
|
|
265
|
+
assert entry["ok"] is True
|
|
266
|
+
assert "time" in entry
|
|
267
|
+
|
|
268
|
+
def test_records_failed_run(self, code_exec_handler):
|
|
269
|
+
with pytest.raises(RuntimeError):
|
|
270
|
+
code_exec_handler.handle_execute_code({"code": "raise ValueError('x')"})
|
|
271
|
+
assert code_exec_handler.get_exec_log()[-1]["ok"] is False
|
|
272
|
+
|
|
273
|
+
def test_log_is_capped(self, code_exec_handler):
|
|
274
|
+
for i in range(code_exec_handler.EXEC_LOG_SIZE + 5):
|
|
275
|
+
code_exec_handler.handle_execute_code({"code": f"x = {i}"})
|
|
276
|
+
assert len(code_exec_handler.get_exec_log()) == code_exec_handler.EXEC_LOG_SIZE
|
|
277
|
+
|
|
278
|
+
def test_echoes_code_to_console(self, code_exec_handler, capsys):
|
|
279
|
+
"""The snippet is printed to the real stdout, not the captured buffer."""
|
|
280
|
+
code_exec_handler.handle_execute_code({"code": "print('seen')"})
|
|
281
|
+
err = capsys.readouterr().err
|
|
282
|
+
assert "[blenderwright] execute_code" in err
|
|
283
|
+
assert "print('seen')" in err
|
|
284
|
+
|
|
285
|
+
def test_get_exec_log_returns_copy(self, code_exec_handler):
|
|
286
|
+
log = code_exec_handler.get_exec_log()
|
|
287
|
+
log.clear()
|
|
288
|
+
assert isinstance(code_exec_handler.get_exec_log(), list)
|
|
@@ -58,3 +58,23 @@ class TestCustomPort:
|
|
|
58
58
|
"""unregister() removes blenderwright_port from Scene."""
|
|
59
59
|
# Should delete the property during unregister
|
|
60
60
|
assert "del" in ui_source and "blenderwright_port" in ui_source
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestRawPythonSwitch:
|
|
64
|
+
def _switch_line(self, ui_source):
|
|
65
|
+
lines = [ln for ln in ui_source.splitlines() if "SWITCH_PROP" in ln and ".prop(" in ln]
|
|
66
|
+
assert len(lines) == 1, "expected exactly one prop() call for the raw Python switch"
|
|
67
|
+
return lines[0]
|
|
68
|
+
|
|
69
|
+
def test_switch_is_a_checkbox_not_a_toggle_button(self, ui_source):
|
|
70
|
+
"""A BoolProperty drawn with icon= becomes a toggle button that reads as
|
|
71
|
+
a one-shot 'Allow' action. No icon, so it draws as a checkbox."""
|
|
72
|
+
assert "icon=" not in self._switch_line(ui_source)
|
|
73
|
+
assert "toggle=" not in self._switch_line(ui_source)
|
|
74
|
+
|
|
75
|
+
def test_switch_lives_on_window_manager(self, ui_source):
|
|
76
|
+
assert "context.window_manager" in ui_source
|
|
77
|
+
assert "WindowManager, code_exec.SWITCH_PROP" in ui_source
|
|
78
|
+
|
|
79
|
+
def test_panel_states_off_when_unticked(self, ui_source):
|
|
80
|
+
assert "execute_blender_code is off" in ui_source
|
|
@@ -53,3 +53,19 @@ class TestExecuteBlenderCode:
|
|
|
53
53
|
|
|
54
54
|
with pytest.raises(RuntimeError, match="Blender error"):
|
|
55
55
|
execute_blender_code(code="print(")
|
|
56
|
+
|
|
57
|
+
def test_disabled_in_blender_raises_with_hint(self, mock_conn):
|
|
58
|
+
"""When the user has not enabled raw Python, the error says how to."""
|
|
59
|
+
mock_conn.send_command.return_value = {
|
|
60
|
+
"status": "error",
|
|
61
|
+
"result": "PermissionError: execute_blender_code is disabled. "
|
|
62
|
+
"Tick 'Allow raw Python' in the blenderwright panel.",
|
|
63
|
+
}
|
|
64
|
+
with pytest.raises(RuntimeError, match="Allow raw Python"):
|
|
65
|
+
execute_blender_code(code="print('x')")
|
|
66
|
+
|
|
67
|
+
def test_docstring_says_off_by_default(self):
|
|
68
|
+
"""The model must learn from the tool description that it is opt-in."""
|
|
69
|
+
doc = execute_blender_code.__doc__ or ""
|
|
70
|
+
assert "off by default" in doc.lower()
|
|
71
|
+
assert "not a security boundary" in doc.lower()
|