blenderwright 2.0.1__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.
Files changed (206) hide show
  1. {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/workflows/ci.yml +9 -1
  2. {blenderwright-2.0.1 → blenderwright-2.2.0}/PKG-INFO +42 -9
  3. {blenderwright-2.0.1 → blenderwright-2.2.0}/README.md +41 -8
  4. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/__init__.py +1 -1
  5. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/blender_manifest.toml +1 -1
  6. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/__init__.py +2 -0
  7. blenderwright-2.2.0/addon/handlers/code_exec.py +176 -0
  8. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/objects.py +35 -0
  9. blenderwright-2.2.0/addon/handlers/sequencer.py +212 -0
  10. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/ui_panel.py +34 -0
  11. {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/index.html +171 -40
  12. blenderwright-2.2.0/docs/shuttle-launch.mp4 +0 -0
  13. blenderwright-2.2.0/glama.json +6 -0
  14. {blenderwright-2.0.1 → blenderwright-2.2.0}/launch_ollama.sh +3 -2
  15. {blenderwright-2.0.1 → blenderwright-2.2.0}/pyproject.toml +1 -1
  16. blenderwright-2.2.0/shuttle-launch.gif +0 -0
  17. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/ollama_chat.py +427 -52
  18. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/server.py +26 -1
  19. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/armature.py +2 -1
  20. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/booltool.py +13 -1
  21. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/camera.py +4 -3
  22. blenderwright-2.2.0/src/blenderwright/tools/code_exec.py +39 -0
  23. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/collections.py +2 -2
  24. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/curves.py +3 -2
  25. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/geometry_nodes.py +2 -2
  26. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/gpencil.py +2 -1
  27. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/lighting.py +5 -3
  28. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/materials.py +23 -7
  29. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/mesh_editing.py +4 -1
  30. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/modeling.py +2 -1
  31. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/objects.py +83 -9
  32. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/rendering.py +16 -8
  33. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/scene.py +2 -1
  34. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/selection.py +15 -8
  35. blenderwright-2.2.0/src/blenderwright/tools/sequencer.py +221 -0
  36. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/sweep.py +2 -2
  37. blenderwright-2.2.0/src/blenderwright/toolsets.py +299 -0
  38. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/validators.py +81 -4
  39. blenderwright-2.2.0/tests/test_addon/conftest.py +28 -0
  40. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_code_exec_handler.py +99 -0
  41. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_objects_handler.py +115 -0
  42. blenderwright-2.2.0/tests/test_addon/test_handlers/test_sequencer_handler.py +261 -0
  43. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_ui_panel.py +20 -0
  44. blenderwright-2.2.0/tests/test_long_names.py +131 -0
  45. blenderwright-2.2.0/tests/test_ollama_budget.py +232 -0
  46. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_ollama_chat.py +5 -2
  47. blenderwright-2.2.0/tests/test_ollama_toolsets.py +152 -0
  48. blenderwright-2.2.0/tests/test_ollama_vision.py +195 -0
  49. blenderwright-2.2.0/tests/test_round_wasters.py +152 -0
  50. blenderwright-2.2.0/tests/test_tool_disambiguation.py +90 -0
  51. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_camera.py +3 -1
  52. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_code_exec.py +16 -0
  53. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_collections.py +4 -3
  54. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_issue_31_validation.py +3 -1
  55. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_materials.py +4 -3
  56. blenderwright-2.2.0/tests/test_tools/test_negative_scale_primitives.py +103 -0
  57. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_rendering.py +18 -13
  58. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_scene.py +4 -3
  59. blenderwright-2.2.0/tests/test_tools/test_sequencer.py +235 -0
  60. blenderwright-2.2.0/tests/test_toolsets.py +251 -0
  61. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_validators.py +22 -4
  62. {blenderwright-2.0.1 → blenderwright-2.2.0}/uv.lock +220 -209
  63. blenderwright-2.0.1/addon/handlers/code_exec.py +0 -101
  64. blenderwright-2.0.1/src/blenderwright/tools/code_exec.py +0 -31
  65. blenderwright-2.0.1/tests/test_addon/conftest.py +0 -11
  66. {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/FUNDING.yml +0 -0
  67. {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/workflows/pylint.yml +0 -0
  68. {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/workflows/release.yml +0 -0
  69. {blenderwright-2.0.1 → blenderwright-2.2.0}/.github/workflows/test-bpy51.yml +0 -0
  70. {blenderwright-2.0.1 → blenderwright-2.2.0}/.gitignore +0 -0
  71. {blenderwright-2.0.1 → blenderwright-2.2.0}/LICENSE.md +0 -0
  72. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/dispatcher.py +0 -0
  73. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/animation.py +0 -0
  74. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/armature.py +0 -0
  75. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/booltool.py +0 -0
  76. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/camera.py +0 -0
  77. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/collections.py +0 -0
  78. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/curves.py +0 -0
  79. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/file_ops.py +0 -0
  80. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/geometry_nodes.py +0 -0
  81. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/gpencil.py +0 -0
  82. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/lighting.py +0 -0
  83. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/materials.py +0 -0
  84. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/mesh_editing.py +0 -0
  85. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/mesh_quality.py +0 -0
  86. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/modeling.py +0 -0
  87. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/physics.py +0 -0
  88. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/print3d.py +0 -0
  89. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/procedural.py +0 -0
  90. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/raster.py +0 -0
  91. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/rendering.py +0 -0
  92. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/scene.py +0 -0
  93. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/sculpting.py +0 -0
  94. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/selection.py +0 -0
  95. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/sweep.py +0 -0
  96. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/transforms.py +0 -0
  97. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/uv.py +0 -0
  98. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/handlers/viewport.py +0 -0
  99. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/render_guard.py +0 -0
  100. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/server.py +0 -0
  101. {blenderwright-2.0.1 → blenderwright-2.2.0}/addon/thread_safety.py +0 -0
  102. {blenderwright-2.0.1 → blenderwright-2.2.0}/blend-ai-v1.0.0.zip +0 -0
  103. {blenderwright-2.0.1 → blenderwright-2.2.0}/build.ps1 +0 -0
  104. {blenderwright-2.0.1 → blenderwright-2.2.0}/build.sh +0 -0
  105. {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/.nojekyll +0 -0
  106. {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/CNAME +0 -0
  107. {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/googled2f6c71abc2d6a7b.html +0 -0
  108. {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/robots.txt +0 -0
  109. {blenderwright-2.0.1 → blenderwright-2.2.0}/docs/sitemap.xml +0 -0
  110. {blenderwright-2.0.1 → blenderwright-2.2.0}/install-addon.sh +0 -0
  111. {blenderwright-2.0.1 → blenderwright-2.2.0}/install_addon.py +0 -0
  112. {blenderwright-2.0.1 → blenderwright-2.2.0}/saloon.png +0 -0
  113. {blenderwright-2.0.1 → blenderwright-2.2.0}/screenshot.png +0 -0
  114. {blenderwright-2.0.1 → blenderwright-2.2.0}/scripts/generate_tool_docs.py +0 -0
  115. {blenderwright-2.0.1 → blenderwright-2.2.0}/shader-preview.png +0 -0
  116. {blenderwright-2.0.1 → blenderwright-2.2.0}/shuttle-launch.png +0 -0
  117. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/__init__.py +0 -0
  118. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/__main__.py +0 -0
  119. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/aliases.py +0 -0
  120. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/connection.py +0 -0
  121. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/enum_hints.py +0 -0
  122. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/prompts/__init__.py +0 -0
  123. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/prompts/workflows.py +0 -0
  124. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/resources/__init__.py +0 -0
  125. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/resources/scene_info.py +0 -0
  126. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/strict.py +0 -0
  127. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tool_registry.py +0 -0
  128. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/__init__.py +0 -0
  129. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/animation.py +0 -0
  130. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/file_ops.py +0 -0
  131. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/mesh_quality.py +0 -0
  132. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/physics.py +0 -0
  133. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/print3d.py +0 -0
  134. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/screenshot.py +0 -0
  135. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/sculpting.py +0 -0
  136. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/transforms.py +0 -0
  137. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/uv.py +0 -0
  138. {blenderwright-2.0.1 → blenderwright-2.2.0}/src/blenderwright/tools/viewport.py +0 -0
  139. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/__init__.py +0 -0
  140. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/conftest.py +0 -0
  141. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/__init__.py +0 -0
  142. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_dispatcher.py +0 -0
  143. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handler_registration.py +0 -0
  144. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/__init__.py +0 -0
  145. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_camera_handler.py +0 -0
  146. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_geometry_nodes_handler.py +0 -0
  147. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_gpencil_handler.py +0 -0
  148. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_materials_handler.py +0 -0
  149. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_mesh_quality_handler.py +0 -0
  150. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_modeling_handler.py +0 -0
  151. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_print3d_handler.py +0 -0
  152. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_procedural_handler.py +0 -0
  153. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_raster_handler.py +0 -0
  154. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_rendering_handler.py +0 -0
  155. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_scene_handler.py +0 -0
  156. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sculpting_handler.py +0 -0
  157. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_selection_handler.py +0 -0
  158. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_handlers/test_sweep_handler.py +0 -0
  159. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_layer_consistency.py +0 -0
  160. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_module_cache_purge.py +0 -0
  161. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_render_guard.py +0 -0
  162. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_server.py +0 -0
  163. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_addon/test_thread_safety.py +0 -0
  164. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_command_registry.py +0 -0
  165. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_connection.py +0 -0
  166. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_enum_hints.py +0 -0
  167. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_installer.py +0 -0
  168. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_object_param_naming.py +0 -0
  169. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_project_metadata.py +0 -0
  170. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_strict_parameters.py +0 -0
  171. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/__init__.py +0 -0
  172. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/conftest.py +0 -0
  173. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_animation.py +0 -0
  174. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_armature.py +0 -0
  175. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_auto_critique_prompt.py +0 -0
  176. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_booltool.py +0 -0
  177. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_curves.py +0 -0
  178. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_eevee_light_path.py +0 -0
  179. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_extensions.py +0 -0
  180. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_file_ops.py +0 -0
  181. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_geometry_nodes.py +0 -0
  182. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_gpencil.py +0 -0
  183. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_lighting.py +0 -0
  184. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_mesh_editing.py +0 -0
  185. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_mesh_quality.py +0 -0
  186. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_mesh_repair.py +0 -0
  187. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_modeling.py +0 -0
  188. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_new_nodes.py +0 -0
  189. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_numeric_text_values.py +0 -0
  190. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_objects.py +0 -0
  191. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_physics.py +0 -0
  192. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_point_camera_at.py +0 -0
  193. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_print3d.py +0 -0
  194. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_procedural.py +0 -0
  195. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_prompts.py +0 -0
  196. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_raster.py +0 -0
  197. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_screenshot.py +0 -0
  198. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_sculpting.py +0 -0
  199. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_selection.py +0 -0
  200. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_selection_respected.py +0 -0
  201. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_shader_nodes.py +0 -0
  202. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_sweep.py +0 -0
  203. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_transforms.py +0 -0
  204. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_uv.py +0 -0
  205. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_uv_slim.py +0 -0
  206. {blenderwright-2.0.1 → blenderwright-2.2.0}/tests/test_tools/test_viewport.py +0 -0
@@ -37,11 +37,19 @@ jobs:
37
37
  run: bandit -r src/ -ll # low severity threshold; skip install_addon.py (subprocess by design)
38
38
  - name: Install project dependencies
39
39
  run: pip install -e ".[dev]"
40
- - name: pip-audit
40
+ - name: pip-audit (fresh resolution)
41
41
  # Audits the environment, which now contains this project's declared
42
42
  # dependencies. The upgrade step above keeps the runner's own build
43
43
  # tooling current so drift in the image does not fail the build.
44
44
  run: pip-audit
45
+ - name: pip-audit (uv.lock)
46
+ # The step above resolves today's versions and passed for weeks while
47
+ # uv.lock, which is what `uv run` and every developer actually
48
+ # install, carried 64 known vulnerabilities. Audit the pins too.
49
+ run: |
50
+ pip install uv
51
+ uv export --all-extras --format requirements-txt --no-hashes --no-emit-project -o /tmp/uv-lock.txt
52
+ pip-audit --no-deps --disable-pip --strict -r /tmp/uv-lock.txt
45
53
 
46
54
  tests:
47
55
  name: Tests (${{ matrix.os }} / Python ${{ matrix.python-version }})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: blenderwright
3
- Version: 2.0.1
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
@@ -24,26 +24,32 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
24
24
 
25
25
  **blenderwright goes beyond tool exposure: it guides the LLM to produce professional 3D results** through expert prompts, proven workflows, visual feedback, and mesh quality analysis.
26
26
 
27
- <small>A two-storey Western saloon from a single sentence: "create a Western style 2-story saloon, the outside is more important than any inside detail." Claude Code (Fable 5.1) built the false front, plank siding, balcony, batwing doors and street dressing, then relit it for dusk, all through blenderwright, in about twenty minutes with no manual modelling:</small>
27
+ <small>A shuttle launch from two questions: "do you want to have a go at the space shuttle launch?" and then "could you animate the launch?" Claude Code (Fable 5.1) modelled the orbiter, tank, boosters and service tower, lit the plumes as emissive volumes and grew the exhaust cloud from metaballs. Keyframes lift the stack and tilt the camera after it, drivers stream the plumes and boil the smoke, and all 60 Cycles frames come back from a single `render_animation` call, all through blenderwright with no manual modelling:</small>
28
28
 
29
- ![blenderwright saloon built from one prompt](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/saloon.png)
29
+ ![blenderwright space shuttle launch, animated](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/shuttle-launch.gif)
30
+
31
+ <small>[Watch it with sound](https://blenderwright.holdmybeer.gg/shuttle-launch.mp4). Launch audio courtesy of NASA: STS-131, "Sound of Launch."</small>
30
32
 
31
- <small>A shuttle launch from one question: "do you want to have a go at the space shuttle launch?" Claude Code (Fable 5.1) modelled the orbiter, tank, boosters and service tower, lit the plumes as emissive volumes, grew the exhaust cloud from metaballs and rendered it in Cycles, all through blenderwright, in under twenty minutes with no manual modelling:</small>
33
+ <small>The still it grew from, modelled and rendered in under twenty minutes:</small>
32
34
 
33
35
  ![blenderwright space shuttle launch built from one prompt](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/shuttle-launch.png)
34
36
 
37
+ <small>A two-storey Western saloon from a single sentence: "create a Western style 2-story saloon, the outside is more important than any inside detail." Claude Code (Fable 5.1) built the false front, plank siding, balcony, batwing doors and street dressing, then relit it for dusk, all through blenderwright, in about twenty minutes with no manual modelling:</small>
38
+
39
+ ![blenderwright saloon built from one prompt](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/saloon.png)
40
+
35
41
  <small>Fifteen procedural materials, each built by a single `create_procedural_material` call. The selected ball's node graph below was generated entirely by that one call: coordinates, mapping, noise, height mask, colour ramp, and Principled BSDF, laid out and wired:</small>
36
42
 
37
43
  ![blenderwright procedural shader preview](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/shader-preview.png)
38
44
 
39
45
  ## Key Features
40
46
 
41
- - **[186 tools](https://blenderwright.holdmybeer.gg/)** across 27 modules covering every major Blender domain: modeling, mesh editing, materials, shader nodes, lighting, camera, animation, rendering, sculpting, UV mapping, physics, geometry nodes, rigging, curves, sweeps along a path, 3D-print checking, annotations, collections, file I/O, Bool Tool, viewport control, mesh quality analysis, and extension suggestions
47
+ - **[191 tools](https://blenderwright.holdmybeer.gg/)** across 28 modules covering every major Blender domain: modeling, mesh editing, materials, shader nodes, lighting, camera, animation, rendering, sculpting, UV mapping, physics, geometry nodes, rigging, curves, sweeps along a path, 3D-print checking, annotations, collections, file I/O, Bool Tool, viewport control, mesh quality analysis, and extension suggestions
42
48
  - **12 expert prompts**: topology best practices, real-world scale references, lighting principles, studio setup, character basemesh workflow, PBR material guide, auto-critique feedback loop, and more
43
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
44
50
  - **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
45
51
  - **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
46
- - **Sandboxed code execution**: `execute_blender_code` blocks dangerous imports (`os`, `subprocess`, `socket`, etc.) and dangerous builtins (`exec`, `eval`, `open`) while allowing safe Blender operations
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
47
53
  - **Render-aware**: automatically detects when Blender is rendering and queues commands. Recovers from stuck render guards via `load_post` handler and reset command
48
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
49
55
  - **Custom port**: configure the server port from the N-panel UI (default: 9876, range: 1024–65535)
@@ -250,7 +256,7 @@ blenderwright includes 12 MCP prompts that guide the LLM toward professional-qua
250
256
  ## Tool Domains
251
257
 
252
258
  <details>
253
- <summary><strong>All 186 tools across 27 modules</strong></summary>
259
+ <summary><strong>All 191 tools across 28 modules</strong></summary>
254
260
 
255
261
  Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://blenderwright.holdmybeer.gg/)**
256
262
 
@@ -272,6 +278,7 @@ Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://ble
272
278
  | Curves | 10 | Bezier/NURBS/path, 3D text, convert, reverse, handle types, cyclic, subdivide |
273
279
  | Sweep | 2 | Sweep a profile along a 3D path for tubes, hoses, cables and rails, with flip-free framing; check a path for bends too tight for the profile |
274
280
  | 3D Printing | 1 | Reversed normals, self-intersection, thin walls, shells and overhangs via Blender's 3D Print Toolbox |
281
+ | Sequencer | 5 | Image-sequence strips, sound strips, list and remove strips, render to MP4, MKV or WebM with audio |
275
282
  | Sculpting | 8 | Brushes, remesh, multires, symmetry, dynamic topology, stroke_method |
276
283
  | UV Mapping | 4 | Smart project, unwrap (ANGLE_BASED, CONFORMAL, SLIM), projection, pack islands |
277
284
  | Physics | 9 | Rigid body, cloth, fluid, particles (velocity, rendering, delete), bake |
@@ -286,6 +293,31 @@ Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://ble
286
293
 
287
294
  </details>
288
295
 
296
+ ### Toolsets
297
+
298
+ Every tool is loaded by default, and that is the setting the featured renders were made with. The full set of schemas costs roughly 48k tokens per request, which is fine for Claude-class clients and too much for a local model on a 32k window. `BLENDERWRIGHT_TOOLSETS` (or `--toolsets`) trims it:
299
+
300
+ | Value | Loads |
301
+ |-------|-------|
302
+ | unset or `all` | everything (default) |
303
+ | `auto` | the core toolsets plus `list_toolsets` and `enable_toolset`; the model loads other groups as a task needs them |
304
+ | `core` | scene, objects, transforms, modeling, mesh quality, lighting, camera, rendering, screenshot, viewport, collections |
305
+ | `core,physics,animation` | any comma-separated list of toolset names; a toolset is one row of the table above |
306
+
307
+ ```json
308
+ {
309
+ "mcpServers": {
310
+ "blenderwright": {
311
+ "command": "uvx",
312
+ "args": ["blenderwright"],
313
+ "env": { "BLENDERWRIGHT_TOOLSETS": "auto" }
314
+ }
315
+ }
316
+ }
317
+ ```
318
+
319
+ `auto` relies on the client honouring the MCP `tools/list_changed` notification. The bundled Ollama chat client picks `auto` on its own when the full set would take more than half of `--num-ctx`, and says so at startup.
320
+
289
321
  ## Architecture
290
322
 
291
323
  ```
@@ -317,7 +349,8 @@ AI Assistant <--stdio/MCP--> blenderwright server <--TCP socket--> Blender addon
317
349
  <summary><strong>Security</strong></summary>
318
350
 
319
351
  - **Localhost only**: The TCP socket binds to `127.0.0.1`, never exposed to the network.
320
- - **Sandboxed code execution**: `execute_blender_code` blocks 25 dangerous 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 removes dangerous builtins (`__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help`, `memoryview`). Safe Blender imports (`bpy`, `bmesh`, `mathutils`, `math`, `json`) are allowed.
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.
321
354
  - **Input validation**: All inputs pass through validators before reaching Blender: name sanitization, path traversal prevention, numeric range checks, enum allowlists.
322
355
  - **File safety**: Import operations disable `use_scripts_auto_execute` to prevent script injection from imported files. File extensions are checked against allowlists.
323
356
  - **Command allowlist**: The addon dispatcher only processes explicitly registered commands. Unknown commands are rejected.
@@ -369,7 +402,7 @@ blenderwright/
369
402
  │ ├── server.py # FastMCP entry point
370
403
  │ ├── connection.py # TCP client to Blender (with busy-retry)
371
404
  │ ├── validators.py # Input validation
372
- │ ├── tools/ # 27 tool modules (186 tools)
405
+ │ ├── tools/ # 28 tool modules (191 tools)
373
406
  │ ├── resources/ # MCP resources (scene, objects, materials)
374
407
  │ └── prompts/ # 12 expert prompt templates
375
408
  ├── addon/ # Blender addon (zero external deps)
@@ -6,26 +6,32 @@ The most intuitive and efficient MCP Server for Blender. Control Blender entirel
6
6
 
7
7
  **blenderwright goes beyond tool exposure: it guides the LLM to produce professional 3D results** through expert prompts, proven workflows, visual feedback, and mesh quality analysis.
8
8
 
9
- <small>A two-storey Western saloon from a single sentence: "create a Western style 2-story saloon, the outside is more important than any inside detail." Claude Code (Fable 5.1) built the false front, plank siding, balcony, batwing doors and street dressing, then relit it for dusk, all through blenderwright, in about twenty minutes with no manual modelling:</small>
9
+ <small>A shuttle launch from two questions: "do you want to have a go at the space shuttle launch?" and then "could you animate the launch?" Claude Code (Fable 5.1) modelled the orbiter, tank, boosters and service tower, lit the plumes as emissive volumes and grew the exhaust cloud from metaballs. Keyframes lift the stack and tilt the camera after it, drivers stream the plumes and boil the smoke, and all 60 Cycles frames come back from a single `render_animation` call, all through blenderwright with no manual modelling:</small>
10
10
 
11
- ![blenderwright saloon built from one prompt](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/saloon.png)
11
+ ![blenderwright space shuttle launch, animated](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/shuttle-launch.gif)
12
+
13
+ <small>[Watch it with sound](https://blenderwright.holdmybeer.gg/shuttle-launch.mp4). Launch audio courtesy of NASA: STS-131, "Sound of Launch."</small>
12
14
 
13
- <small>A shuttle launch from one question: "do you want to have a go at the space shuttle launch?" Claude Code (Fable 5.1) modelled the orbiter, tank, boosters and service tower, lit the plumes as emissive volumes, grew the exhaust cloud from metaballs and rendered it in Cycles, all through blenderwright, in under twenty minutes with no manual modelling:</small>
15
+ <small>The still it grew from, modelled and rendered in under twenty minutes:</small>
14
16
 
15
17
  ![blenderwright space shuttle launch built from one prompt](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/shuttle-launch.png)
16
18
 
19
+ <small>A two-storey Western saloon from a single sentence: "create a Western style 2-story saloon, the outside is more important than any inside detail." Claude Code (Fable 5.1) built the false front, plank siding, balcony, batwing doors and street dressing, then relit it for dusk, all through blenderwright, in about twenty minutes with no manual modelling:</small>
20
+
21
+ ![blenderwright saloon built from one prompt](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/saloon.png)
22
+
17
23
  <small>Fifteen procedural materials, each built by a single `create_procedural_material` call. The selected ball's node graph below was generated entirely by that one call: coordinates, mapping, noise, height mask, colour ramp, and Principled BSDF, laid out and wired:</small>
18
24
 
19
25
  ![blenderwright procedural shader preview](https://raw.githubusercontent.com/HoldMyBeer-gg/blenderwright/main/shader-preview.png)
20
26
 
21
27
  ## Key Features
22
28
 
23
- - **[186 tools](https://blenderwright.holdmybeer.gg/)** across 27 modules covering every major Blender domain: modeling, mesh editing, materials, shader nodes, lighting, camera, animation, rendering, sculpting, UV mapping, physics, geometry nodes, rigging, curves, sweeps along a path, 3D-print checking, annotations, collections, file I/O, Bool Tool, viewport control, mesh quality analysis, and extension suggestions
29
+ - **[191 tools](https://blenderwright.holdmybeer.gg/)** across 28 modules covering every major Blender domain: modeling, mesh editing, materials, shader nodes, lighting, camera, animation, rendering, sculpting, UV mapping, physics, geometry nodes, rigging, curves, sweeps along a path, 3D-print checking, annotations, collections, file I/O, Bool Tool, viewport control, mesh quality analysis, and extension suggestions
24
30
  - **12 expert prompts**: topology best practices, real-world scale references, lighting principles, studio setup, character basemesh workflow, PBR material guide, auto-critique feedback loop, and more
25
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
26
32
  - **Mesh quality analysis**: structured reports covering non-manifold edges, loose vertices, zero-area faces, duplicate vertices, and wire edges
27
33
  - **Extension suggestions**: proactively recommends Bool Tool, LoopTools, and Node Wrangler when a task would benefit from them (skips already-installed extensions)
28
- - **Sandboxed code execution**: `execute_blender_code` blocks dangerous imports (`os`, `subprocess`, `socket`, etc.) and dangerous builtins (`exec`, `eval`, `open`) while allowing safe Blender operations
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
29
35
  - **Render-aware**: automatically detects when Blender is rendering and queues commands. Recovers from stuck render guards via `load_post` handler and reset command
30
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
31
37
  - **Custom port**: configure the server port from the N-panel UI (default: 9876, range: 1024–65535)
@@ -232,7 +238,7 @@ blenderwright includes 12 MCP prompts that guide the LLM toward professional-qua
232
238
  ## Tool Domains
233
239
 
234
240
  <details>
235
- <summary><strong>All 186 tools across 27 modules</strong></summary>
241
+ <summary><strong>All 191 tools across 28 modules</strong></summary>
236
242
 
237
243
  Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://blenderwright.holdmybeer.gg/)**
238
244
 
@@ -254,6 +260,7 @@ Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://ble
254
260
  | Curves | 10 | Bezier/NURBS/path, 3D text, convert, reverse, handle types, cyclic, subdivide |
255
261
  | Sweep | 2 | Sweep a profile along a 3D path for tubes, hoses, cables and rails, with flip-free framing; check a path for bends too tight for the profile |
256
262
  | 3D Printing | 1 | Reversed normals, self-intersection, thin walls, shells and overhangs via Blender's 3D Print Toolbox |
263
+ | Sequencer | 5 | Image-sequence strips, sound strips, list and remove strips, render to MP4, MKV or WebM with audio |
257
264
  | Sculpting | 8 | Brushes, remesh, multires, symmetry, dynamic topology, stroke_method |
258
265
  | UV Mapping | 4 | Smart project, unwrap (ANGLE_BASED, CONFORMAL, SLIM), projection, pack islands |
259
266
  | Physics | 9 | Rigid body, cloth, fluid, particles (velocity, rendering, delete), bake |
@@ -268,6 +275,31 @@ Full reference with every parameter: **[blenderwright.holdmybeer.gg](https://ble
268
275
 
269
276
  </details>
270
277
 
278
+ ### Toolsets
279
+
280
+ Every tool is loaded by default, and that is the setting the featured renders were made with. The full set of schemas costs roughly 48k tokens per request, which is fine for Claude-class clients and too much for a local model on a 32k window. `BLENDERWRIGHT_TOOLSETS` (or `--toolsets`) trims it:
281
+
282
+ | Value | Loads |
283
+ |-------|-------|
284
+ | unset or `all` | everything (default) |
285
+ | `auto` | the core toolsets plus `list_toolsets` and `enable_toolset`; the model loads other groups as a task needs them |
286
+ | `core` | scene, objects, transforms, modeling, mesh quality, lighting, camera, rendering, screenshot, viewport, collections |
287
+ | `core,physics,animation` | any comma-separated list of toolset names; a toolset is one row of the table above |
288
+
289
+ ```json
290
+ {
291
+ "mcpServers": {
292
+ "blenderwright": {
293
+ "command": "uvx",
294
+ "args": ["blenderwright"],
295
+ "env": { "BLENDERWRIGHT_TOOLSETS": "auto" }
296
+ }
297
+ }
298
+ }
299
+ ```
300
+
301
+ `auto` relies on the client honouring the MCP `tools/list_changed` notification. The bundled Ollama chat client picks `auto` on its own when the full set would take more than half of `--num-ctx`, and says so at startup.
302
+
271
303
  ## Architecture
272
304
 
273
305
  ```
@@ -299,7 +331,8 @@ AI Assistant <--stdio/MCP--> blenderwright server <--TCP socket--> Blender addon
299
331
  <summary><strong>Security</strong></summary>
300
332
 
301
333
  - **Localhost only**: The TCP socket binds to `127.0.0.1`, never exposed to the network.
302
- - **Sandboxed code execution**: `execute_blender_code` blocks 25 dangerous 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 removes dangerous builtins (`__import__`, `exec`, `eval`, `compile`, `open`, `globals`, `locals`, `vars`, `input`, `breakpoint`, `exit`, `quit`, `help`, `memoryview`). Safe Blender imports (`bpy`, `bmesh`, `mathutils`, `math`, `json`) are allowed.
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.
303
336
  - **Input validation**: All inputs pass through validators before reaching Blender: name sanitization, path traversal prevention, numeric range checks, enum allowlists.
304
337
  - **File safety**: Import operations disable `use_scripts_auto_execute` to prevent script injection from imported files. File extensions are checked against allowlists.
305
338
  - **Command allowlist**: The addon dispatcher only processes explicitly registered commands. Unknown commands are rejected.
@@ -351,7 +384,7 @@ blenderwright/
351
384
  │ ├── server.py # FastMCP entry point
352
385
  │ ├── connection.py # TCP client to Blender (with busy-retry)
353
386
  │ ├── validators.py # Input validation
354
- │ ├── tools/ # 27 tool modules (186 tools)
387
+ │ ├── tools/ # 28 tool modules (191 tools)
355
388
  │ ├── resources/ # MCP resources (scene, objects, materials)
356
389
  │ └── prompts/ # 12 expert prompt templates
357
390
  ├── addon/ # Blender addon (zero external deps)
@@ -8,7 +8,7 @@ Blender's Python API.
8
8
  bl_info = {
9
9
  "name": "blenderwright",
10
10
  "author": "blenderwright",
11
- "version": (2, 0, 1),
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",
@@ -1,7 +1,7 @@
1
1
  schema_version = "1.0.0"
2
2
 
3
3
  id = "blenderwright"
4
- version = "2.0.1"
4
+ version = "2.2.0"
5
5
  name = "blenderwright"
6
6
  tagline = "MCP server integration for AI-assisted 3D workflows"
7
7
  maintainer = "jabberwock <88205+jabberwock@users.noreply.github.com>"
@@ -29,6 +29,7 @@ from . import (
29
29
  gpencil,
30
30
  sweep,
31
31
  print3d,
32
+ sequencer,
32
33
  )
33
34
 
34
35
  _modules = [
@@ -60,6 +61,7 @@ _modules = [
60
61
  gpencil,
61
62
  sweep,
62
63
  print3d,
64
+ sequencer,
63
65
  ]
64
66
 
65
67
 
@@ -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 math
4
4
 
5
5
  import bpy
6
+ import mathutils
6
7
  from .. import dispatcher
7
8
 
8
9
  # Map of primitive type to bpy.ops function
@@ -38,6 +39,29 @@ def _get_primitive_op(ptype: str):
38
39
  return ops_map.get(ptype)
39
40
 
40
41
 
42
+ def _extent(obj) -> dict:
43
+ """The object's size and where it ends, in world space.
44
+
45
+ A caller placing the next part needs to know where this one stops. A
46
+ model assumed a primitive was one unit tall, scaled it by 18.5, and put
47
+ the nose cone at z=9.25, halfway up inside a body that reached 18.5.
48
+ Blender's cylinder is two units tall; the result says so now.
49
+ """
50
+ bpy.context.view_layer.update()
51
+ out = {"dimensions": [float(v) for v in obj.dimensions]}
52
+ try:
53
+ corners = [obj.matrix_world @ mathutils.Vector(c) for c in obj.bound_box]
54
+ xs, ys, zs = ([float(getattr(c, axis)) for c in corners] for axis in "xyz")
55
+ out["bounds"] = {
56
+ "min": [min(xs), min(ys), min(zs)],
57
+ "max": [max(xs), max(ys), max(zs)],
58
+ }
59
+ except Exception:
60
+ # No bound box (an empty), or something odd: the dimensions still help.
61
+ pass
62
+ return out
63
+
64
+
41
65
  def handle_create_object(params: dict) -> dict:
42
66
  """Create a primitive object in the scene."""
43
67
  obj_type = params.get("type", "CUBE")
@@ -65,6 +89,7 @@ def handle_create_object(params: dict) -> dict:
65
89
  "name": obj.name,
66
90
  "type": obj.type,
67
91
  "location": list(obj.location),
92
+ **_extent(obj),
68
93
  }
69
94
  except Exception as e:
70
95
  raise RuntimeError(f"Failed to create object: {e}")
@@ -100,6 +125,7 @@ def handle_create_polygon_prism(params: dict) -> dict:
100
125
  "type": obj.type,
101
126
  "location": list(obj.location),
102
127
  "sides": sides,
128
+ **_extent(obj),
103
129
  }
104
130
  except Exception as e:
105
131
  raise RuntimeError(f"Failed to create polygon prism: {e}")
@@ -260,6 +286,7 @@ def handle_create_threaded_shaft(params: dict) -> dict:
260
286
  "pitch": pitch,
261
287
  "thread_depth": thread_depth,
262
288
  "iterations": iterations,
289
+ **_extent(obj),
263
290
  }
264
291
  except Exception as e:
265
292
  raise RuntimeError(f"Failed to create threaded shaft: {e}")
@@ -510,9 +537,17 @@ def handle_join_objects(params: dict) -> dict:
510
537
  bpy.ops.object.join()
511
538
 
512
539
  result_obj = bpy.context.active_object
540
+ # The others are gone. A caller that uses one of their names on the
541
+ # next call gets "not found" and loses a round; say so here instead.
542
+ removed = [n for n in names if n != result_obj.name]
513
543
  return {
514
544
  "name": result_obj.name,
515
545
  "joined_count": len(names),
546
+ "removed": removed,
547
+ "note": (
548
+ f"{', '.join(removed)} no longer exist; their geometry is part of "
549
+ f"{result_obj.name}. Refer to {result_obj.name} from now on."
550
+ ),
516
551
  "success": True,
517
552
  }
518
553
  except ValueError: