@hybridlabor-api/aos 4.4.1 → 4.4.2-beta.0

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 (305) hide show
  1. package/.agents/skills/firecrawl/SKILL.md +149 -0
  2. package/.agents/skills/firecrawl/rules/install.md +82 -0
  3. package/.agents/skills/firecrawl/rules/security.md +26 -0
  4. package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
  5. package/.agents/skills/firecrawl-build/SKILL.md +39 -0
  6. package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
  7. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
  8. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
  9. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
  10. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
  11. package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
  12. package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
  13. package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
  14. package/.agents/skills/firecrawl-download/SKILL.md +70 -0
  15. package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
  16. package/.agents/skills/firecrawl-map/SKILL.md +51 -0
  17. package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
  18. package/.agents/skills/firecrawl-search/SKILL.md +60 -0
  19. package/CLAUDE.md +573 -0
  20. package/README.de.md +0 -8
  21. package/README.md +0 -8
  22. package/README.pt.md +0 -8
  23. package/assets/brand/ao-ant-mark.svg +91 -0
  24. package/assets/brand/bdb-core-mark.svg +34 -0
  25. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  26. package/assets/brand/memb-crystal-mark.svg +18 -0
  27. package/bin/aos-config.mjs +89 -0
  28. package/bin/aos-dashboard.mjs +608 -0
  29. package/bin/aos-uninstall.mjs +227 -0
  30. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  31. package/docs/skills_table.md +0 -3
  32. package/installer.js +387 -237
  33. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  34. package/mcps/after-effects-mcp/build/index.js +840 -0
  35. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  37. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  38. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  39. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  40. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  41. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  42. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  43. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  44. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  45. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  46. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  47. package/mcps/computer-use-mcp/dist/client.js +136 -0
  48. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  50. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  51. package/mcps/computer-use-mcp/dist/native.js +50 -0
  52. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  53. package/mcps/computer-use-mcp/dist/server.js +342 -0
  54. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  55. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  56. package/package.json +13 -4
  57. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  58. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  59. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  60. package/skills/global_config/aos-setup/SKILL.md +98 -8
  61. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +21 -2
  62. package/skills/global_config/ask-tim/SKILL.md +2 -7
  63. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  64. package/bin/setup-saas.mjs +0 -792
  65. package/mcps/blender-mcp-server/.dockerignore +0 -19
  66. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  67. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  68. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  69. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  70. package/mcps/blender-mcp-server/Dockerfile +0 -19
  71. package/mcps/blender-mcp-server/LICENSE +0 -21
  72. package/mcps/blender-mcp-server/README.md +0 -409
  73. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  74. package/mcps/blender-mcp-server/addon/models.py +0 -223
  75. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  76. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  77. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  78. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  79. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  80. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  81. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  82. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  83. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  85. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  86. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  87. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  88. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  89. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  90. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  91. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  92. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  93. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  94. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  95. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  96. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  97. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  98. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  99. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  100. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  101. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  104. package/mcps/davinci-mcp-professional/.bandit +0 -35
  105. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  106. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  107. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  108. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  109. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  110. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  112. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  113. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  114. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  115. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  116. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  117. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  118. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  119. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  120. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  121. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  122. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  123. package/mcps/davinci-mcp-professional/README.md +0 -143
  124. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  125. package/mcps/davinci-mcp-professional/USING.md +0 -301
  126. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  127. package/mcps/davinci-mcp-professional/build.py +0 -169
  128. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  129. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  130. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  131. package/mcps/davinci-mcp-professional/main.py +0 -40
  132. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  133. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  134. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  135. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  144. package/mcps/davinci-mcp-professional/test.py +0 -186
  145. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  146. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  147. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  148. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  149. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  151. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  152. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  153. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  154. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  155. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  156. package/mcps/davinci-resolve-mcp/README.md +0 -173
  157. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  158. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  159. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  160. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  161. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  162. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  163. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  164. package/mcps/davinci-resolve-mcp/package.json +0 -52
  165. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  169. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  170. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  171. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  172. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  173. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  174. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  176. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  177. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  178. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  179. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  180. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  181. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  182. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  183. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  184. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  185. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  187. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  188. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  189. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  190. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  192. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  193. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  194. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  195. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  196. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  200. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  201. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  202. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  203. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  204. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  205. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  206. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  207. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  208. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  209. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  210. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  211. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  212. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  213. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  214. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  215. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  216. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  217. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  218. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  219. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  220. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  222. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  223. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  225. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  227. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  229. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  230. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  231. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  232. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  233. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  234. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  235. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  237. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  238. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  239. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  240. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  241. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  245. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  246. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  247. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  248. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  249. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  250. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  256. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  257. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  258. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  259. package/mcps/vectorworks-mcp/README.md +0 -156
  260. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  261. package/mcps/vectorworks-mcp/app/api.py +0 -52
  262. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  263. package/mcps/vectorworks-mcp/app/config.py +0 -29
  264. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  265. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  266. package/mcps/vectorworks-mcp/app/main.py +0 -33
  267. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  268. package/mcps/vectorworks-mcp/app/search.py +0 -54
  269. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  270. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  271. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  276. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  277. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  279. package/mcps/vectorworks-mcp/design.md +0 -57
  280. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  281. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  282. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  283. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  284. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  285. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  286. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  287. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  288. package/scripts/ecosystem-health-audit.js +0 -257
  289. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  290. package/skills/bdb-dev-os-skill/package.json +0 -11
  291. package/skills/bdbsaastraining/SKILL.md +0 -316
  292. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  293. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  294. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  295. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  296. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  297. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  298. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  299. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  300. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  301. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  302. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  303. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  304. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  305. package/skills/global_config/bdbsaashost/SKILL.md +0 -125
@@ -1,1440 +0,0 @@
1
- bl_info = {
2
- "name": "Blender MCP Bridge",
3
- "author": "Blender MCP Server",
4
- "version": (0, 1, 0),
5
- "blender": (3, 6, 0),
6
- "location": "View3D > Sidebar > MCP",
7
- "description": "TCP bridge for MCP server to control Blender",
8
- "category": "Development",
9
- }
10
-
11
- import builtins
12
- import errno
13
- import io
14
- import json
15
- import logging
16
- import math
17
- import os
18
- import queue
19
- import socket
20
- import sys
21
- import threading
22
- import time
23
- import traceback
24
- import uuid
25
- from contextlib import redirect_stderr, redirect_stdout
26
- from typing import Any
27
-
28
- import bpy
29
- from bpy.app.handlers import persistent
30
-
31
- from .models import (
32
- ExportFileParams,
33
- JobIdParams,
34
- MaterialAssignParams,
35
- MaterialCreateParams,
36
- MaterialSetColorParams,
37
- MaterialSetTextureParams,
38
- ObjectCreateMeshParams,
39
- ObjectDeleteParams,
40
- ObjectDuplicateParams,
41
- ObjectGetHierarchyParams,
42
- ObjectGetTransformParams,
43
- ObjectRotateParams,
44
- ObjectScaleParams,
45
- ObjectTranslateParams,
46
- PythonExecuteParams,
47
- RenderAnimationParams,
48
- RenderStillParams,
49
- SceneListObjectsParams,
50
- )
51
-
52
- logger = logging.getLogger(__name__)
53
-
54
- HOST = "127.0.0.1"
55
- PORT = 9876
56
- BUFFER_SIZE = 65536
57
-
58
- # Security: restrict file operations to these directories (set via addon preferences)
59
- SAFE_MODE = False
60
- ALLOWED_PATHS: list[str] = []
61
- TOOL_WHITELIST: set[str] | None = None # None = all tools allowed
62
-
63
- # Python execution settings (set via addon preferences)
64
- ALLOW_INLINE_CODE = True
65
- APPROVED_SCRIPT_ROOTS: list[str] = []
66
- BLOCKED_MODULES: set[str] = {
67
- "subprocess",
68
- "socket",
69
- "webbrowser",
70
- "ctypes",
71
- "multiprocessing",
72
- }
73
- DEFAULT_SYNC_TIMEOUT = 30
74
- DEFAULT_ASYNC_TIMEOUT = 300
75
- MAX_SYNC_TIMEOUT = 300
76
- MAX_ASYNC_TIMEOUT = 3600
77
- MAX_OUTPUT_SIZE = 50000 # Cap stdout/stderr returned to client
78
- LOG_CODE_PREVIEW_LEN = 120 # Max chars of code shown in log messages
79
-
80
-
81
- def _truncate(text: str, limit: int) -> str:
82
- """Truncate text with an ellipsis indicator."""
83
- if len(text) <= limit:
84
- return text
85
- return text[:limit] + "…"
86
-
87
-
88
- def _cap_output(text: str) -> str:
89
- """Cap output string to MAX_OUTPUT_SIZE."""
90
- if len(text) <= MAX_OUTPUT_SIZE:
91
- return text
92
- return text[:MAX_OUTPUT_SIZE] + f"\n… (truncated, {len(text)} total chars)"
93
-
94
-
95
- def _build_cube_pydata(
96
- size: float,
97
- ) -> tuple[list[tuple[float, float, float]], list[tuple[int, ...]]]:
98
- half = size / 2.0
99
- verts = [
100
- (-half, -half, -half),
101
- (half, -half, -half),
102
- (half, half, -half),
103
- (-half, half, -half),
104
- (-half, -half, half),
105
- (half, -half, half),
106
- (half, half, half),
107
- (-half, half, half),
108
- ]
109
- faces = [
110
- (0, 1, 2, 3),
111
- (4, 5, 6, 7),
112
- (0, 1, 5, 4),
113
- (1, 2, 6, 5),
114
- (2, 3, 7, 6),
115
- (3, 0, 4, 7),
116
- ]
117
- return verts, faces
118
-
119
-
120
- def _build_plane_pydata(
121
- size: float,
122
- ) -> tuple[list[tuple[float, float, float]], list[tuple[int, ...]]]:
123
- half = size / 2.0
124
- verts = [
125
- (-half, -half, 0.0),
126
- (half, -half, 0.0),
127
- (half, half, 0.0),
128
- (-half, half, 0.0),
129
- ]
130
- return verts, [(0, 1, 2, 3)]
131
-
132
-
133
- def _build_cylinder_pydata(
134
- size: float, segments: int = 24
135
- ) -> tuple[list[tuple[float, float, float]], list[tuple[int, ...]]]:
136
- radius = size / 2.0
137
- half_depth = size / 2.0
138
- verts: list[tuple[float, float, float]] = []
139
- for ring_z in (-half_depth, half_depth):
140
- for i in range(segments):
141
- angle = 2.0 * math.pi * i / segments
142
- verts.append((radius * math.cos(angle), radius * math.sin(angle), ring_z))
143
-
144
- faces: list[tuple[int, ...]] = []
145
- for i in range(segments):
146
- nxt = (i + 1) % segments
147
- faces.append((i, nxt, segments + nxt, segments + i))
148
- faces.append(tuple(range(segments - 1, -1, -1)))
149
- faces.append(tuple(range(segments, 2 * segments)))
150
- return verts, faces
151
-
152
-
153
- def _build_cone_pydata(
154
- size: float, segments: int = 24
155
- ) -> tuple[list[tuple[float, float, float]], list[tuple[int, ...]]]:
156
- radius = size / 2.0
157
- half_depth = size / 2.0
158
- verts: list[tuple[float, float, float]] = []
159
- for i in range(segments):
160
- angle = 2.0 * math.pi * i / segments
161
- verts.append((radius * math.cos(angle), radius * math.sin(angle), -half_depth))
162
- apex_index = len(verts)
163
- verts.append((0.0, 0.0, half_depth))
164
-
165
- faces: list[tuple[int, ...]] = []
166
- for i in range(segments):
167
- nxt = (i + 1) % segments
168
- faces.append((i, nxt, apex_index))
169
- faces.append(tuple(range(segments - 1, -1, -1)))
170
- return verts, faces
171
-
172
-
173
- def _build_uv_sphere_pydata(
174
- size: float, segments: int = 24, rings: int = 12
175
- ) -> tuple[list[tuple[float, float, float]], list[tuple[int, ...]]]:
176
- radius = size / 2.0
177
- verts: list[tuple[float, float, float]] = [(0.0, 0.0, radius)]
178
- for ring in range(1, rings):
179
- phi = math.pi * ring / rings
180
- sin_phi = math.sin(phi)
181
- cos_phi = math.cos(phi)
182
- for seg in range(segments):
183
- theta = 2.0 * math.pi * seg / segments
184
- verts.append(
185
- (
186
- radius * sin_phi * math.cos(theta),
187
- radius * sin_phi * math.sin(theta),
188
- radius * cos_phi,
189
- )
190
- )
191
- bottom_index = len(verts)
192
- verts.append((0.0, 0.0, -radius))
193
-
194
- faces: list[tuple[int, ...]] = []
195
- for seg in range(segments):
196
- nxt = (seg + 1) % segments
197
- faces.append((0, 1 + nxt, 1 + seg))
198
-
199
- for ring in range(rings - 2):
200
- ring_start = 1 + ring * segments
201
- next_start = ring_start + segments
202
- for seg in range(segments):
203
- nxt = (seg + 1) % segments
204
- faces.append(
205
- (
206
- ring_start + seg,
207
- ring_start + nxt,
208
- next_start + nxt,
209
- next_start + seg,
210
- )
211
- )
212
-
213
- last_ring_start = 1 + (rings - 2) * segments
214
- for seg in range(segments):
215
- nxt = (seg + 1) % segments
216
- faces.append((last_ring_start + seg, last_ring_start + nxt, bottom_index))
217
- return verts, faces
218
-
219
-
220
- def _build_torus_pydata(
221
- size: float, major_segments: int = 24, minor_segments: int = 12
222
- ) -> tuple[list[tuple[float, float, float]], list[tuple[int, ...]]]:
223
- major_radius = size * 0.35
224
- minor_radius = size * 0.15
225
- verts: list[tuple[float, float, float]] = []
226
- for major in range(major_segments):
227
- major_angle = 2.0 * math.pi * major / major_segments
228
- cos_major = math.cos(major_angle)
229
- sin_major = math.sin(major_angle)
230
- for minor in range(minor_segments):
231
- minor_angle = 2.0 * math.pi * minor / minor_segments
232
- ring = major_radius + minor_radius * math.cos(minor_angle)
233
- verts.append(
234
- (
235
- ring * cos_major,
236
- ring * sin_major,
237
- minor_radius * math.sin(minor_angle),
238
- )
239
- )
240
-
241
- faces: list[tuple[int, ...]] = []
242
- for major in range(major_segments):
243
- major_next = (major + 1) % major_segments
244
- for minor in range(minor_segments):
245
- minor_next = (minor + 1) % minor_segments
246
- v0 = major * minor_segments + minor
247
- v1 = major * minor_segments + minor_next
248
- v2 = major_next * minor_segments + minor_next
249
- v3 = major_next * minor_segments + minor
250
- faces.append((v0, v1, v2, v3))
251
- return verts, faces
252
-
253
-
254
- def _build_primitive_pydata(
255
- mesh_type: str, size: float
256
- ) -> tuple[list[tuple[float, float, float]], list[tuple[int, ...]]]:
257
- builders = {
258
- "cube": _build_cube_pydata,
259
- "sphere": _build_uv_sphere_pydata,
260
- "cylinder": _build_cylinder_pydata,
261
- "plane": _build_plane_pydata,
262
- "cone": _build_cone_pydata,
263
- "torus": _build_torus_pydata,
264
- }
265
- builder = builders.get(mesh_type)
266
- if builder is None:
267
- raise ValueError(f"Unknown mesh type: {mesh_type}. Options: {list(builders.keys())}")
268
- return builder(size)
269
-
270
-
271
- def _create_primitive_mesh_object(
272
- mesh_type: str,
273
- *,
274
- name: str | None = None,
275
- location: list[float] | tuple[float, float, float] | None = None,
276
- size: float = 2.0,
277
- ):
278
- obj_name = name or mesh_type.capitalize()
279
- verts, faces = _build_primitive_pydata(mesh_type.lower(), size)
280
- mesh = bpy.data.meshes.new(f"{obj_name}Mesh")
281
- mesh.from_pydata(verts, [], faces)
282
- mesh.update()
283
- obj = bpy.data.objects.new(obj_name, mesh)
284
- bpy.context.scene.collection.objects.link(obj)
285
- if location is not None:
286
- obj.location = tuple(location)
287
- return obj
288
-
289
-
290
- def _make_exec_builtins(blocked_modules: set[str]) -> dict[str, Any]:
291
- """Create a per-execution builtins dict with a guarded __import__."""
292
- exec_builtins = dict(builtins.__dict__)
293
- original_import = exec_builtins["__import__"]
294
-
295
- def guarded_import(name, *a, **kw):
296
- top_level = name.split(".")[0]
297
- if top_level in blocked_modules:
298
- raise ImportError(f"Import of '{name}' is blocked by MCP safety policy")
299
- return original_import(name, *a, **kw)
300
-
301
- exec_builtins["__import__"] = guarded_import
302
- return exec_builtins
303
-
304
-
305
- # Last execution status — shown in the Blender UI panel
306
- _last_execution: dict[str, Any] = {
307
- "request_id": None,
308
- "source": None,
309
- "status": None,
310
- "duration_seconds": None,
311
- "error_summary": None,
312
- }
313
-
314
-
315
- class ScriptExecutionTimeout(TimeoutError):
316
- """Raised when script execution exceeds the configured timeout."""
317
-
318
-
319
- class ScriptExecutionCancelled(RuntimeError):
320
- """Raised when script execution is cancelled cooperatively."""
321
-
322
-
323
- def _get_addon_preferences():
324
- """Return this add-on's preferences object when available."""
325
- context = getattr(bpy, "context", None)
326
- preferences = getattr(context, "preferences", None)
327
- addons = getattr(preferences, "addons", None)
328
- if addons is None:
329
- return None
330
-
331
- addon_entry = None
332
- getter = getattr(addons, "get", None)
333
- if callable(getter):
334
- addon_entry = getter(__name__)
335
- elif isinstance(addons, dict):
336
- addon_entry = addons.get(__name__)
337
-
338
- return getattr(addon_entry, "preferences", None)
339
-
340
-
341
- def _sync_runtime_settings():
342
- """Apply add-on preferences to module-level runtime settings."""
343
- global SAFE_MODE, PORT, ALLOW_INLINE_CODE, APPROVED_SCRIPT_ROOTS, ALLOWED_PATHS
344
-
345
- prefs = _get_addon_preferences()
346
- if prefs is None:
347
- return
348
-
349
- SAFE_MODE = bool(getattr(prefs, "safe_mode", SAFE_MODE))
350
- PORT = int(getattr(prefs, "port", PORT))
351
- ALLOW_INLINE_CODE = bool(getattr(prefs, "allow_inline_code", ALLOW_INLINE_CODE))
352
-
353
- raw_roots = getattr(prefs, "approved_script_roots", "") or ""
354
- path_helper = getattr(getattr(bpy, "path", None), "abspath", None)
355
- approved_roots = []
356
- for root in raw_roots.split(";"):
357
- root = root.strip()
358
- if not root:
359
- continue
360
- resolved = path_helper(root) if callable(path_helper) else root
361
- approved_roots.append(os.path.realpath(resolved))
362
-
363
- APPROVED_SCRIPT_ROOTS = approved_roots
364
- ALLOWED_PATHS = approved_roots.copy() if SAFE_MODE else []
365
-
366
-
367
- class CommandHandler:
368
- """Dispatches JSON commands to the appropriate bpy operations."""
369
-
370
- def __init__(self):
371
- self._handlers: dict[str, callable] = {}
372
- self._register_builtins()
373
-
374
- def _register_builtins(self):
375
- self._handlers["scene.get_info"] = self._scene_get_info
376
- self._handlers["scene.list_objects"] = self._scene_list_objects
377
- self._handlers["object.get_transform"] = self._object_get_transform
378
- self._handlers["object.get_hierarchy"] = self._object_get_hierarchy
379
- self._handlers["material.list"] = self._material_list
380
- self._handlers["object.create_mesh"] = self._object_create_mesh
381
- self._handlers["object.delete"] = self._object_delete
382
- self._handlers["object.translate"] = self._object_translate
383
- self._handlers["object.rotate"] = self._object_rotate
384
- self._handlers["object.scale"] = self._object_scale
385
- self._handlers["object.duplicate"] = self._object_duplicate
386
- self._handlers["material.create"] = self._material_create
387
- self._handlers["material.assign"] = self._material_assign
388
- self._handlers["material.set_color"] = self._material_set_color
389
- self._handlers["material.set_texture"] = self._material_set_texture
390
- self._handlers["render.still"] = self._render_still
391
- self._handlers["render.animation"] = self._render_animation
392
- self._handlers["export.gltf"] = self._export_gltf
393
- self._handlers["export.obj"] = self._export_obj
394
- self._handlers["export.fbx"] = self._export_fbx
395
- self._handlers["history.undo"] = self._history_undo
396
- self._handlers["history.redo"] = self._history_redo
397
- self._handlers["python.execute"] = self._python_execute
398
- self._handlers["python.execute_async"] = self._python_execute_async
399
- self._handlers["job.status"] = self._job_status
400
- self._handlers["job.cancel"] = self._job_cancel
401
- self._handlers["job.list"] = self._job_list
402
-
403
- _VALIDATORS: dict[str, type] = {
404
- "scene.list_objects": SceneListObjectsParams,
405
- "object.get_transform": ObjectGetTransformParams,
406
- "object.get_hierarchy": ObjectGetHierarchyParams,
407
- "object.create_mesh": ObjectCreateMeshParams,
408
- "object.delete": ObjectDeleteParams,
409
- "object.translate": ObjectTranslateParams,
410
- "object.rotate": ObjectRotateParams,
411
- "object.scale": ObjectScaleParams,
412
- "object.duplicate": ObjectDuplicateParams,
413
- "material.create": MaterialCreateParams,
414
- "material.assign": MaterialAssignParams,
415
- "material.set_color": MaterialSetColorParams,
416
- "material.set_texture": MaterialSetTextureParams,
417
- "render.still": RenderStillParams,
418
- "render.animation": RenderAnimationParams,
419
- "export.gltf": ExportFileParams,
420
- "export.obj": ExportFileParams,
421
- "export.fbx": ExportFileParams,
422
- "python.execute": PythonExecuteParams,
423
- "python.execute_async": PythonExecuteParams,
424
- "job.status": JobIdParams,
425
- "job.cancel": JobIdParams,
426
- }
427
-
428
- def handle(self, command: str, params: dict) -> Any:
429
- _sync_runtime_settings()
430
- # Security: check tool whitelist
431
- if TOOL_WHITELIST is not None and command not in TOOL_WHITELIST:
432
- raise PermissionError(f"Command '{command}' is not in the tool whitelist")
433
- handler = self._handlers.get(command)
434
- if not handler:
435
- raise ValueError(f"Unknown command: {command}")
436
- validator = self._VALIDATORS.get(command)
437
- if validator is not None:
438
- params = validator.model_validate(params).model_dump(exclude_none=True)
439
- return handler(params)
440
-
441
- # -- Scene tools --
442
-
443
- @staticmethod
444
- def _validate_filepath(filepath: str) -> str:
445
- """Security: validate that a filepath is within allowed directories."""
446
- if not SAFE_MODE:
447
- return filepath
448
- abs_path = os.path.abspath(bpy.path.abspath(filepath))
449
- if not ALLOWED_PATHS:
450
- # In safe mode with no allowed paths, only allow the blend file directory
451
- blend_dir = os.path.dirname(bpy.data.filepath) if bpy.data.filepath else os.getcwd()
452
- ALLOWED_PATHS.append(blend_dir)
453
- for allowed in ALLOWED_PATHS:
454
- if abs_path.startswith(os.path.abspath(allowed)):
455
- return filepath
456
- raise PermissionError(
457
- f"File access denied: '{filepath}' is outside allowed directories. Allowed: {ALLOWED_PATHS}"
458
- )
459
-
460
- def _scene_get_info(self, params: dict) -> dict:
461
- scene = bpy.context.scene
462
- return {
463
- "name": scene.name,
464
- "frame_current": scene.frame_current,
465
- "frame_start": scene.frame_start,
466
- "frame_end": scene.frame_end,
467
- "render_engine": scene.render.engine,
468
- "resolution_x": scene.render.resolution_x,
469
- "resolution_y": scene.render.resolution_y,
470
- "object_count": len(scene.objects),
471
- }
472
-
473
- def _scene_list_objects(self, params: dict) -> dict:
474
- type_filter = params.get("type")
475
- objects = []
476
- for obj in bpy.context.scene.objects:
477
- if type_filter and obj.type != type_filter.upper():
478
- continue
479
- objects.append(
480
- {
481
- "name": obj.name,
482
- "type": obj.type,
483
- "location": list(obj.location),
484
- "visible": obj.visible_get(),
485
- }
486
- )
487
- return {"objects": objects}
488
-
489
- def _object_get_transform(self, params: dict) -> dict:
490
- name = params["name"]
491
- obj = bpy.data.objects.get(name)
492
- if not obj:
493
- raise ValueError(f"Object '{name}' not found")
494
- return {
495
- "name": obj.name,
496
- "location": list(obj.location),
497
- "rotation_euler": list(obj.rotation_euler),
498
- "scale": list(obj.scale),
499
- }
500
-
501
- def _object_get_hierarchy(self, params: dict) -> dict:
502
- def build_tree(obj):
503
- return {
504
- "name": obj.name,
505
- "type": obj.type,
506
- "children": [build_tree(c) for c in obj.children],
507
- }
508
-
509
- name = params.get("name")
510
- if name:
511
- obj = bpy.data.objects.get(name)
512
- if not obj:
513
- raise ValueError(f"Object '{name}' not found")
514
- return build_tree(obj)
515
-
516
- roots = [o for o in bpy.context.scene.objects if o.parent is None]
517
- return {"roots": [build_tree(r) for r in roots]}
518
-
519
- def _material_list(self, params: dict) -> dict:
520
- materials = []
521
- for mat in bpy.data.materials:
522
- materials.append(
523
- {
524
- "name": mat.name,
525
- "use_nodes": mat.use_nodes,
526
- "user_count": mat.users,
527
- }
528
- )
529
- return {"materials": materials}
530
-
531
- # -- Object mutation tools --
532
-
533
- def _object_create_mesh(self, params: dict) -> dict:
534
- mesh_type = params.get("type", "cube").lower()
535
- name = params.get("name")
536
- location = params.get("location", [0, 0, 0])
537
- size = params.get("size", 2.0)
538
- obj = _create_primitive_mesh_object(
539
- mesh_type,
540
- name=name,
541
- location=location,
542
- size=size,
543
- )
544
- return {"name": obj.name, "type": obj.type, "location": list(obj.location)}
545
-
546
- def _object_delete(self, params: dict) -> dict:
547
- name = params["name"]
548
- obj = bpy.data.objects.get(name)
549
- if not obj:
550
- raise ValueError(f"Object '{name}' not found")
551
- bpy.data.objects.remove(obj, do_unlink=True)
552
- return {"deleted": name}
553
-
554
- def _object_translate(self, params: dict) -> dict:
555
- name = params["name"]
556
- obj = bpy.data.objects.get(name)
557
- if not obj:
558
- raise ValueError(f"Object '{name}' not found")
559
- offset = params.get("offset", [0, 0, 0])
560
- absolute = params.get("location")
561
- if absolute:
562
- obj.location = absolute
563
- else:
564
- obj.location.x += offset[0]
565
- obj.location.y += offset[1]
566
- obj.location.z += offset[2]
567
- return {"name": obj.name, "location": list(obj.location)}
568
-
569
- def _object_rotate(self, params: dict) -> dict:
570
- import math
571
-
572
- name = params["name"]
573
- obj = bpy.data.objects.get(name)
574
- if not obj:
575
- raise ValueError(f"Object '{name}' not found")
576
- rotation = params.get("rotation", [0, 0, 0])
577
- degrees = params.get("degrees", True)
578
- if degrees:
579
- rotation = [math.radians(r) for r in rotation]
580
- obj.rotation_euler = rotation
581
- return {"name": obj.name, "rotation_euler": list(obj.rotation_euler)}
582
-
583
- def _object_scale(self, params: dict) -> dict:
584
- name = params["name"]
585
- obj = bpy.data.objects.get(name)
586
- if not obj:
587
- raise ValueError(f"Object '{name}' not found")
588
- scale = params.get("scale", [1, 1, 1])
589
- obj.scale = scale
590
- return {"name": obj.name, "scale": list(obj.scale)}
591
-
592
- def _object_duplicate(self, params: dict) -> dict:
593
- name = params["name"]
594
- obj = bpy.data.objects.get(name)
595
- if not obj:
596
- raise ValueError(f"Object '{name}' not found")
597
- new_obj = obj.copy()
598
- new_obj.data = obj.data.copy()
599
- new_name = params.get("new_name")
600
- if new_name:
601
- new_obj.name = new_name
602
- bpy.context.collection.objects.link(new_obj)
603
- return {
604
- "name": new_obj.name,
605
- "original": obj.name,
606
- "location": list(new_obj.location),
607
- }
608
-
609
- # -- Material tools --
610
-
611
- def _material_create(self, params: dict) -> dict:
612
- name = params.get("name", "Material")
613
- mat = bpy.data.materials.new(name=name)
614
- mat.use_nodes = True
615
- color = params.get("color")
616
- if color:
617
- bsdf = mat.node_tree.nodes.get("Principled BSDF")
618
- if bsdf:
619
- # color is [r, g, b] or [r, g, b, a], values 0-1
620
- rgba = list(color) + [1.0] * (4 - len(color))
621
- bsdf.inputs["Base Color"].default_value = rgba[:4]
622
- return {"name": mat.name, "use_nodes": mat.use_nodes}
623
-
624
- def _material_assign(self, params: dict) -> dict:
625
- obj_name = params["object"]
626
- mat_name = params["material"]
627
- obj = bpy.data.objects.get(obj_name)
628
- if not obj:
629
- raise ValueError(f"Object '{obj_name}' not found")
630
- mat = bpy.data.materials.get(mat_name)
631
- if not mat:
632
- raise ValueError(f"Material '{mat_name}' not found")
633
- if obj.data.materials:
634
- obj.data.materials[0] = mat
635
- else:
636
- obj.data.materials.append(mat)
637
- return {"object": obj.name, "material": mat.name}
638
-
639
- def _material_set_color(self, params: dict) -> dict:
640
- mat_name = params["name"]
641
- color = params["color"] # [r, g, b] or [r, g, b, a]
642
- mat = bpy.data.materials.get(mat_name)
643
- if not mat:
644
- raise ValueError(f"Material '{mat_name}' not found")
645
- if not mat.use_nodes:
646
- mat.use_nodes = True
647
- bsdf = mat.node_tree.nodes.get("Principled BSDF")
648
- if not bsdf:
649
- raise ValueError(f"Material '{mat_name}' has no Principled BSDF node")
650
- rgba = list(color) + [1.0] * (4 - len(color))
651
- bsdf.inputs["Base Color"].default_value = rgba[:4]
652
- return {"name": mat.name, "color": rgba[:4]}
653
-
654
- def _material_set_texture(self, params: dict) -> dict:
655
- mat_name = params["name"]
656
- filepath = params["filepath"]
657
- self._validate_filepath(filepath)
658
- mat = bpy.data.materials.get(mat_name)
659
- if not mat:
660
- raise ValueError(f"Material '{mat_name}' not found")
661
- if not mat.use_nodes:
662
- mat.use_nodes = True
663
- tree = mat.node_tree
664
- bsdf = tree.nodes.get("Principled BSDF")
665
- if not bsdf:
666
- raise ValueError(f"Material '{mat_name}' has no Principled BSDF node")
667
- tex_node = tree.nodes.new("ShaderNodeTexImage")
668
- tex_node.image = bpy.data.images.load(filepath)
669
- tree.links.new(tex_node.outputs["Color"], bsdf.inputs["Base Color"])
670
- return {"name": mat.name, "texture": filepath}
671
-
672
- # -- Render tools --
673
-
674
- def _render_still(self, params: dict) -> dict:
675
- scene = bpy.context.scene
676
- output_path = params.get("output_path", "//render.png")
677
- self._validate_filepath(output_path)
678
- resolution_x = params.get("resolution_x")
679
- resolution_y = params.get("resolution_y")
680
- engine = params.get("engine")
681
-
682
- if engine:
683
- scene.render.engine = engine.upper()
684
- if resolution_x:
685
- scene.render.resolution_x = resolution_x
686
- if resolution_y:
687
- scene.render.resolution_y = resolution_y
688
-
689
- scene.render.filepath = output_path
690
- bpy.ops.render.render(write_still=True)
691
- abs_path = bpy.path.abspath(output_path)
692
- return {
693
- "output_path": abs_path,
694
- "engine": scene.render.engine,
695
- "resolution": [scene.render.resolution_x, scene.render.resolution_y],
696
- }
697
-
698
- def _render_animation(self, params: dict) -> dict:
699
- scene = bpy.context.scene
700
- output_path = params.get("output_path", "//render_")
701
- self._validate_filepath(output_path)
702
- frame_start = params.get("frame_start")
703
- frame_end = params.get("frame_end")
704
- engine = params.get("engine")
705
-
706
- if engine:
707
- scene.render.engine = engine.upper()
708
- if frame_start is not None:
709
- scene.frame_start = frame_start
710
- if frame_end is not None:
711
- scene.frame_end = frame_end
712
-
713
- scene.render.filepath = output_path
714
- bpy.ops.render.render(animation=True)
715
- abs_path = bpy.path.abspath(output_path)
716
- return {
717
- "output_path": abs_path,
718
- "frame_range": [scene.frame_start, scene.frame_end],
719
- "engine": scene.render.engine,
720
- }
721
-
722
- # -- Export tools --
723
-
724
- def _export_gltf(self, params: dict) -> dict:
725
- filepath = params["filepath"]
726
- self._validate_filepath(filepath)
727
- if not filepath.endswith((".glb", ".gltf")):
728
- filepath += ".glb"
729
- bpy.ops.export_scene.gltf(filepath=filepath)
730
- return {"filepath": filepath, "format": "glTF"}
731
-
732
- def _export_obj(self, params: dict) -> dict:
733
- filepath = params["filepath"]
734
- self._validate_filepath(filepath)
735
- if not filepath.endswith(".obj"):
736
- filepath += ".obj"
737
- bpy.ops.wm.obj_export(filepath=filepath)
738
- return {"filepath": filepath, "format": "OBJ"}
739
-
740
- def _export_fbx(self, params: dict) -> dict:
741
- filepath = params["filepath"]
742
- self._validate_filepath(filepath)
743
- if not filepath.endswith(".fbx"):
744
- filepath += ".fbx"
745
- bpy.ops.export_scene.fbx(filepath=filepath)
746
- return {"filepath": filepath, "format": "FBX"}
747
-
748
- # -- History tools --
749
-
750
- def _history_undo(self, params: dict) -> dict:
751
- bpy.ops.ed.undo()
752
- return {"action": "undo"}
753
-
754
- def _history_redo(self, params: dict) -> dict:
755
- bpy.ops.ed.redo()
756
- return {"action": "redo"}
757
-
758
- # -- Python execution tools --
759
-
760
- @staticmethod
761
- def _validate_script_path(script_path: str) -> str:
762
- """Validate that a script path is under an approved root directory."""
763
- real_path = os.path.realpath(script_path)
764
- if not os.path.isfile(real_path):
765
- raise FileNotFoundError(f"Script not found: {script_path}")
766
- if not real_path.endswith(".py"):
767
- raise ValueError(f"Script must be a .py file: {script_path}")
768
-
769
- roots = APPROVED_SCRIPT_ROOTS
770
- if not roots:
771
- blend_dir = os.path.dirname(bpy.data.filepath) if bpy.data.filepath else os.getcwd()
772
- roots = [blend_dir]
773
-
774
- for root in roots:
775
- if real_path.startswith(os.path.realpath(root) + os.sep) or real_path == os.path.realpath(root):
776
- return real_path
777
- raise PermissionError(f"Script path denied: '{script_path}' is outside approved roots. Approved: {roots}")
778
-
779
- @staticmethod
780
- def _make_namespace(args: dict, cancel_event: threading.Event | None = None) -> dict:
781
- """Build the execution namespace for exec()."""
782
- import mathutils
783
-
784
- ns: dict[str, Any] = {
785
- "bpy": bpy,
786
- "math": math,
787
- "mathutils": mathutils,
788
- "mcp_create_mesh": _create_primitive_mesh_object,
789
- "args": args or {},
790
- "__result__": None,
791
- "__builtins__": _make_exec_builtins(BLOCKED_MODULES),
792
- }
793
- if cancel_event is not None:
794
- ns["__cancel_event__"] = cancel_event
795
- return ns
796
-
797
- @staticmethod
798
- def _safe_json(value: Any) -> Any:
799
- """Ensure a value is JSON-serializable, falling back to repr()."""
800
- if value is None:
801
- return None
802
- try:
803
- json.dumps(value)
804
- return value
805
- except (TypeError, ValueError):
806
- return repr(value)
807
-
808
- def _run_code(
809
- self,
810
- code: str,
811
- namespace: dict,
812
- timeout_seconds: float,
813
- request_id: str | None = None,
814
- cancel_event: threading.Event | None = None,
815
- ) -> dict:
816
- """Execute code with stdout/stderr capture and cooperative timeout checks."""
817
- stdout_buf = io.StringIO()
818
- stderr_buf = io.StringIO()
819
- start = time.monotonic()
820
- previous_trace = sys.gettrace()
821
-
822
- def trace_calls(frame, event, arg):
823
- if event == "line":
824
- if cancel_event is not None and cancel_event.is_set():
825
- raise ScriptExecutionCancelled("Execution cancelled")
826
- if timeout_seconds > 0 and (time.monotonic() - start) > timeout_seconds:
827
- raise ScriptExecutionTimeout(f"Execution exceeded timeout of {timeout_seconds:.3f}s")
828
- return trace_calls
829
-
830
- sys.settrace(trace_calls)
831
- try:
832
- with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
833
- exec(compile(code, "<mcp-script>", "exec"), namespace)
834
- except Exception as exc:
835
- elapsed = time.monotonic() - start
836
- tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
837
- # Filter out internal bridge frames
838
- filtered = [line for line in tb_lines if "addon/__init__" not in line]
839
- error_str = "".join(filtered).strip()
840
- logger.warning(
841
- "Script execution failed [%s] after %.3fs: %s",
842
- request_id or "?",
843
- elapsed,
844
- _truncate(str(exc), 200),
845
- )
846
- return {
847
- "result": None,
848
- "stdout": _cap_output(stdout_buf.getvalue()),
849
- "stderr": _cap_output(stderr_buf.getvalue()),
850
- "error": error_str,
851
- "duration_seconds": round(elapsed, 4),
852
- "timed_out": isinstance(exc, ScriptExecutionTimeout),
853
- "cancelled": isinstance(exc, ScriptExecutionCancelled),
854
- }
855
- finally:
856
- sys.settrace(previous_trace)
857
-
858
- elapsed = time.monotonic() - start
859
- logger.info(
860
- "Script execution succeeded [%s] in %.3fs",
861
- request_id or "?",
862
- elapsed,
863
- )
864
- return {
865
- "result": self._safe_json(namespace.get("__result__")),
866
- "stdout": _cap_output(stdout_buf.getvalue()),
867
- "stderr": _cap_output(stderr_buf.getvalue()),
868
- "error": None,
869
- "duration_seconds": round(elapsed, 4),
870
- "timed_out": False,
871
- "cancelled": False,
872
- }
873
-
874
- def _python_execute(self, params: dict) -> dict:
875
- global _last_execution
876
- code = params.get("code")
877
- script_path = params.get("script_path")
878
- args = params.get("args", {})
879
- timeout_seconds = min(params.get("timeout_seconds", DEFAULT_SYNC_TIMEOUT), MAX_SYNC_TIMEOUT)
880
-
881
- request_id = f"exec-{uuid.uuid4().hex[:8]}"
882
-
883
- if code and script_path:
884
- raise ValueError("Provide either 'code' or 'script_path', not both")
885
- if not code and not script_path:
886
- raise ValueError("Either 'code' or 'script_path' must be provided")
887
-
888
- if code is not None:
889
- if not ALLOW_INLINE_CODE:
890
- raise PermissionError("Inline code execution is disabled. Use script_path instead.")
891
- source_label = f"inline ({_truncate(code, LOG_CODE_PREVIEW_LEN)})"
892
- else:
893
- validated = self._validate_script_path(script_path)
894
- with open(validated) as f:
895
- code = f.read()
896
- source_label = f"file ({script_path})"
897
-
898
- logger.info("python.execute [%s] starting: %s", request_id, source_label)
899
-
900
- namespace = self._make_namespace(args)
901
- result = self._run_code(code, namespace, timeout_seconds, request_id, cancel_event=None)
902
-
903
- _last_execution = {
904
- "request_id": request_id,
905
- "source": source_label,
906
- "status": "error" if result.get("error") else "ok",
907
- "duration_seconds": result.get("duration_seconds"),
908
- "error_summary": (_truncate(result["error"], 200) if result.get("error") else None),
909
- }
910
- return result
911
-
912
- def _python_execute_async(self, params: dict) -> dict:
913
- code = params.get("code")
914
- script_path = params.get("script_path")
915
- args = params.get("args", {})
916
- timeout_seconds = min(params.get("timeout_seconds", DEFAULT_ASYNC_TIMEOUT), MAX_ASYNC_TIMEOUT)
917
-
918
- if code and script_path:
919
- raise ValueError("Provide either 'code' or 'script_path', not both")
920
- if not code and not script_path:
921
- raise ValueError("Either 'code' or 'script_path' must be provided")
922
-
923
- if code is not None:
924
- if not ALLOW_INLINE_CODE:
925
- raise PermissionError("Inline code execution is disabled. Use script_path instead.")
926
- source_label = f"inline ({_truncate(code, LOG_CODE_PREVIEW_LEN)})"
927
- else:
928
- validated = self._validate_script_path(script_path)
929
- with open(validated) as f:
930
- code = f.read()
931
- source_label = f"file ({script_path})"
932
-
933
- job_id = _job_manager.create_job(code, args, timeout_seconds, self)
934
- logger.info("python.execute_async [%s] queued: %s", job_id, source_label)
935
- return {"job_id": job_id}
936
-
937
- def _job_status(self, params: dict) -> dict:
938
- job_id = params.get("job_id")
939
- if not job_id:
940
- raise ValueError("'job_id' is required")
941
- return _job_manager.get_status(job_id)
942
-
943
- def _job_cancel(self, params: dict) -> dict:
944
- job_id = params.get("job_id")
945
- if not job_id:
946
- raise ValueError("'job_id' is required")
947
- return _job_manager.cancel(job_id)
948
-
949
- def _job_list(self, params: dict) -> dict:
950
- return _job_manager.list_jobs()
951
-
952
-
953
- class JobManager:
954
- """Manages async job lifecycle for long-running Blender scripts."""
955
-
956
- def __init__(self):
957
- self._jobs: dict[str, dict[str, Any]] = {}
958
- self._lock = threading.Lock()
959
-
960
- def create_job(
961
- self,
962
- code: str,
963
- args: dict,
964
- timeout_seconds: float,
965
- handler: CommandHandler,
966
- ) -> str:
967
- job_id = f"job-{uuid.uuid4().hex[:8]}"
968
- now = time.time()
969
- cancel_event = threading.Event()
970
-
971
- job = {
972
- "job_id": job_id,
973
- "status": "queued",
974
- "cancellation_requested": False,
975
- "created_at": now,
976
- "started_at": None,
977
- "completed_at": None,
978
- "result": None,
979
- "stdout": "",
980
- "stderr": "",
981
- "error": None,
982
- "code": code,
983
- "args": args,
984
- "timeout_seconds": timeout_seconds,
985
- "cancel_event": cancel_event,
986
- "handler": handler,
987
- }
988
-
989
- with self._lock:
990
- self._jobs[job_id] = job
991
-
992
- bpy.app.timers.register(lambda: self._execute_job(job_id), first_interval=0.01)
993
- return job_id
994
-
995
- def _execute_job(self, job_id: str) -> None:
996
- global _last_execution
997
- with self._lock:
998
- job = self._jobs.get(job_id)
999
- if not job or job["status"] != "queued":
1000
- return
1001
- job["status"] = "running"
1002
- job["started_at"] = time.time()
1003
-
1004
- logger.info("Job [%s] running", job_id)
1005
-
1006
- cancel_event = job["cancel_event"]
1007
- if cancel_event.is_set():
1008
- with self._lock:
1009
- job["status"] = "cancelled"
1010
- job["completed_at"] = time.time()
1011
- logger.info("Job [%s] cancelled before start", job_id)
1012
- return
1013
-
1014
- handler = job["handler"]
1015
- namespace = handler._make_namespace(job["args"], cancel_event)
1016
- result = handler._run_code(
1017
- job["code"],
1018
- namespace,
1019
- job["timeout_seconds"],
1020
- job_id,
1021
- cancel_event=cancel_event,
1022
- )
1023
-
1024
- with self._lock:
1025
- job["result"] = result.get("result")
1026
- job["stdout"] = result.get("stdout", "")
1027
- job["stderr"] = result.get("stderr", "")
1028
- job["error"] = result.get("error")
1029
- if result.get("cancelled") or cancel_event.is_set():
1030
- job["status"] = "cancelled"
1031
- else:
1032
- job["status"] = "failed" if result.get("error") else "succeeded"
1033
- job["completed_at"] = time.time()
1034
-
1035
- elapsed = (job["completed_at"] - job["started_at"]) if job["started_at"] else 0
1036
- logger.info("Job [%s] %s in %.3fs", job_id, job["status"], elapsed)
1037
-
1038
- _last_execution = {
1039
- "request_id": job_id,
1040
- "source": "async job",
1041
- "status": job["status"],
1042
- "duration_seconds": round(elapsed, 4),
1043
- "error_summary": _truncate(job["error"], 200) if job.get("error") else None,
1044
- }
1045
-
1046
- def get_status(self, job_id: str) -> dict:
1047
- with self._lock:
1048
- job = self._jobs.get(job_id)
1049
- if not job:
1050
- raise ValueError(f"Unknown job: {job_id}")
1051
- return {
1052
- "job_id": job["job_id"],
1053
- "status": job["status"],
1054
- "cancellation_requested": job["cancellation_requested"],
1055
- "created_at": job["created_at"],
1056
- "started_at": job["started_at"],
1057
- "completed_at": job["completed_at"],
1058
- "result": job["result"],
1059
- "stdout": job["stdout"],
1060
- "stderr": job["stderr"],
1061
- "error": job["error"],
1062
- }
1063
-
1064
- def cancel(self, job_id: str) -> dict:
1065
- with self._lock:
1066
- job = self._jobs.get(job_id)
1067
- if not job:
1068
- raise ValueError(f"Unknown job: {job_id}")
1069
-
1070
- job["cancel_event"].set()
1071
- job["cancellation_requested"] = True
1072
-
1073
- if job["status"] == "queued":
1074
- job["status"] = "cancelled"
1075
- job["completed_at"] = time.time()
1076
-
1077
- status = job["status"]
1078
- cancellation_requested = job["cancellation_requested"]
1079
-
1080
- return {
1081
- "job_id": job_id,
1082
- "status": status,
1083
- "cancellation_requested": cancellation_requested,
1084
- }
1085
-
1086
- def list_jobs(self) -> dict:
1087
- with self._lock:
1088
- jobs = [
1089
- {
1090
- "job_id": j["job_id"],
1091
- "status": j["status"],
1092
- "created_at": j["created_at"],
1093
- }
1094
- for j in self._jobs.values()
1095
- ]
1096
- return {"jobs": jobs}
1097
-
1098
-
1099
- # Global job manager instance
1100
- _job_manager = JobManager()
1101
-
1102
-
1103
- class BlenderMCPServer:
1104
- """TCP socket server running inside Blender."""
1105
-
1106
- def __init__(self):
1107
- self._server_socket: socket.socket | None = None
1108
- self._thread: threading.Thread | None = None
1109
- self._running = False
1110
- self._host = HOST
1111
- self._port = PORT
1112
- self._handler = CommandHandler()
1113
- self._request_queue: queue.Queue[dict[str, Any]] = queue.Queue()
1114
- self._drain_timer_callback = self._drain_request_queue
1115
-
1116
- def start(self):
1117
- if self._running:
1118
- self._register_request_queue_timer()
1119
- return
1120
- _sync_runtime_settings()
1121
- self._host = HOST
1122
- self._port = PORT
1123
- self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1124
- self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1125
- self._server_socket.settimeout(1.0)
1126
- try:
1127
- self._server_socket.bind((self._host, self._port))
1128
- except OSError as exc:
1129
- if self._server_socket is not None:
1130
- self._server_socket.close()
1131
- self._server_socket = None
1132
- if exc.errno == errno.EADDRINUSE:
1133
- logger.warning(
1134
- "Blender MCP Bridge port %s already in use; skipping bridge startup",
1135
- self._port,
1136
- )
1137
- self._running = False
1138
- return
1139
- raise
1140
- self._running = True
1141
- self._server_socket.listen(1)
1142
- self._thread = threading.Thread(target=self._accept_loop, daemon=True)
1143
- self._thread.start()
1144
- self._register_request_queue_timer()
1145
- logger.info(f"Blender MCP Bridge listening on {self._host}:{self._port}")
1146
-
1147
- def _register_request_queue_timer(self):
1148
- timers = bpy.app.timers
1149
- is_registered = getattr(timers, "is_registered", None)
1150
- if callable(is_registered):
1151
- try:
1152
- if is_registered(self._drain_timer_callback):
1153
- return
1154
- except Exception:
1155
- logger.debug("Unable to check MCP Bridge queue timer registration", exc_info=True)
1156
- try:
1157
- timers.register(self._drain_timer_callback, first_interval=0.01, persistent=True)
1158
- except TypeError:
1159
- # Blender versions before the persistent timer flag still need the bridge to run.
1160
- timers.register(self._drain_timer_callback, first_interval=0.01)
1161
- logger.debug("Registered MCP Bridge request queue timer")
1162
-
1163
- def stop(self):
1164
- self._running = False
1165
- if self._server_socket:
1166
- self._server_socket.close()
1167
- self._server_socket = None
1168
- if self._thread:
1169
- self._thread.join(timeout=3)
1170
- self._thread = None
1171
- while not self._request_queue.empty():
1172
- queued = self._request_queue.get_nowait()
1173
- queued["response"] = {
1174
- "id": queued["request"].get("id"),
1175
- "success": False,
1176
- "error": "Blender MCP Bridge stopped",
1177
- }
1178
- queued["event"].set()
1179
- logger.info("Blender MCP Bridge stopped")
1180
-
1181
- def _accept_loop(self):
1182
- while self._running:
1183
- try:
1184
- conn, addr = self._server_socket.accept()
1185
- logger.info(f"MCP client connected from {addr}")
1186
- client_thread = threading.Thread(target=self._handle_client, args=(conn,), daemon=True)
1187
- client_thread.start()
1188
- except TimeoutError:
1189
- continue
1190
- except OSError:
1191
- break
1192
-
1193
- def _handle_client(self, conn: socket.socket):
1194
- conn.settimeout(None)
1195
- buffer = b""
1196
- try:
1197
- while self._running:
1198
- data = conn.recv(BUFFER_SIZE)
1199
- if not data:
1200
- break
1201
- buffer += data
1202
- # Messages are newline-delimited JSON
1203
- while b"\n" in buffer:
1204
- line, buffer = buffer.split(b"\n", 1)
1205
- if not line.strip():
1206
- continue
1207
- try:
1208
- request = json.loads(line)
1209
- response = self._submit_request(request)
1210
- except json.JSONDecodeError as e:
1211
- response = {
1212
- "id": None,
1213
- "success": False,
1214
- "error": f"Invalid JSON: {e}",
1215
- }
1216
- conn.sendall(json.dumps(response).encode() + b"\n")
1217
- except Exception as e:
1218
- logger.error(f"Client handler error: {e}")
1219
- finally:
1220
- conn.close()
1221
-
1222
- # Commands that modify scene state and need undo push
1223
- MUTATION_COMMANDS = {
1224
- "object.create_mesh",
1225
- "object.delete",
1226
- "object.translate",
1227
- "object.rotate",
1228
- "object.scale",
1229
- "object.duplicate",
1230
- "material.create",
1231
- "material.assign",
1232
- "material.set_color",
1233
- "material.set_texture",
1234
- }
1235
-
1236
- def _submit_request(self, request: dict) -> dict:
1237
- queued = {"request": request, "event": threading.Event(), "response": None}
1238
- logger.debug("Enqueue MCP request id=%s command=%s", request.get("id"), request.get("command"))
1239
- self._request_queue.put(queued)
1240
- queued["event"].wait()
1241
- return queued["response"]
1242
-
1243
- def _drain_request_queue(self):
1244
- while True:
1245
- try:
1246
- queued = self._request_queue.get_nowait()
1247
- except queue.Empty:
1248
- break
1249
- request = queued["request"]
1250
- logger.debug(
1251
- "Processing MCP request id=%s command=%s",
1252
- request.get("id"),
1253
- request.get("command"),
1254
- )
1255
- queued["response"] = self._process_request(queued["request"])
1256
- logger.debug(
1257
- "Finished MCP request id=%s command=%s",
1258
- request.get("id"),
1259
- request.get("command"),
1260
- )
1261
- queued["event"].set()
1262
- return 0.01 if self._running else None
1263
-
1264
- @staticmethod
1265
- def _maybe_push_undo(command: str):
1266
- undo_push = bpy.ops.ed.undo_push
1267
- poll = getattr(undo_push, "poll", None)
1268
- if callable(poll) and not poll():
1269
- logger.debug(f"Skipping undo push for {command}: context poll failed")
1270
- return
1271
- undo_push(message=f"MCP: {command}")
1272
-
1273
- def _process_request(self, request: dict) -> dict:
1274
- req_id = request.get("id")
1275
- command = request.get("command", "")
1276
- params = request.get("params", {})
1277
- try:
1278
- # Auto-push undo before mutations
1279
- if command in self.MUTATION_COMMANDS:
1280
- self._maybe_push_undo(command)
1281
- result = self._handler.handle(command, params)
1282
- return {"id": req_id, "success": True, "result": result}
1283
- except Exception as e:
1284
- logger.error(f"Command '{command}' failed: {e}\n{traceback.format_exc()}")
1285
- return {"id": req_id, "success": False, "error": str(e)}
1286
-
1287
-
1288
- # Global server instance
1289
- _server: BlenderMCPServer | None = None
1290
-
1291
-
1292
- def _server_healthy() -> bool:
1293
- global _server
1294
- return bool(
1295
- _server
1296
- and _server._running
1297
- and _server._server_socket is not None
1298
- and _server._thread is not None
1299
- and _server._thread.is_alive()
1300
- )
1301
-
1302
-
1303
- def _ensure_server_running():
1304
- global _server
1305
- if _server_healthy():
1306
- _server._register_request_queue_timer()
1307
- return None
1308
- if _server:
1309
- try:
1310
- _server.stop()
1311
- except Exception:
1312
- logger.exception("Failed stopping stale MCP Bridge during restart")
1313
- _sync_runtime_settings()
1314
- _server = BlenderMCPServer()
1315
- _server.start()
1316
- return None
1317
-
1318
-
1319
- @persistent
1320
- def _on_load_post(_dummy):
1321
- bpy.app.timers.register(_ensure_server_running, first_interval=0.1)
1322
-
1323
-
1324
- class MCP_AddonPreferences(bpy.types.AddonPreferences):
1325
- bl_idname = __name__
1326
-
1327
- safe_mode: bpy.props.BoolProperty(
1328
- name="Safe Mode",
1329
- description="Restrict file access to project directory and enable tool whitelist",
1330
- default=False,
1331
- )
1332
- port: bpy.props.IntProperty(
1333
- name="Port",
1334
- description="TCP port for the MCP bridge",
1335
- default=9876,
1336
- min=1024,
1337
- max=65535,
1338
- )
1339
- allow_inline_code: bpy.props.BoolProperty(
1340
- name="Allow Inline Code",
1341
- description="Allow python.execute to run inline code strings. Disable to only allow script files",
1342
- default=True,
1343
- )
1344
- approved_script_roots: bpy.props.StringProperty(
1345
- name="Approved Script Roots",
1346
- description="Semicolon-separated list of directories from which script files may be loaded",
1347
- default="",
1348
- )
1349
-
1350
- def draw(self, context):
1351
- layout = self.layout
1352
- layout.prop(self, "safe_mode")
1353
- layout.prop(self, "port")
1354
- layout.separator()
1355
- layout.label(text="Python Execution")
1356
- layout.prop(self, "allow_inline_code")
1357
- layout.prop(self, "approved_script_roots")
1358
-
1359
-
1360
- class MCP_PT_Panel(bpy.types.Panel):
1361
- bl_label = "MCP Bridge"
1362
- bl_idname = "MCP_PT_panel"
1363
- bl_space_type = "VIEW_3D"
1364
- bl_region_type = "UI"
1365
- bl_category = "MCP"
1366
-
1367
- def draw(self, context):
1368
- layout = self.layout
1369
- global _server
1370
- if _server and _server._running:
1371
- layout.label(text=f"● Listening on {_server._host}:{_server._port}", icon="LINKED")
1372
- layout.operator("mcp.stop_server", text="Stop Server")
1373
- else:
1374
- layout.label(text="○ Server stopped", icon="UNLINKED")
1375
- layout.operator("mcp.start_server", text="Start Server")
1376
-
1377
- # Last script execution status
1378
- if _last_execution["request_id"]:
1379
- layout.separator()
1380
- layout.label(text="Last Execution", icon="SCRIPT")
1381
- box = layout.box()
1382
- status = _last_execution["status"]
1383
- icon = "CHECKMARK" if status in ("ok", "succeeded") else "ERROR"
1384
- box.label(text=f"Status: {status}", icon=icon)
1385
- box.label(text=f"ID: {_last_execution['request_id']}")
1386
- if _last_execution["duration_seconds"] is not None:
1387
- box.label(text=f"Duration: {_last_execution['duration_seconds']:.3f}s")
1388
- if _last_execution["error_summary"]:
1389
- box.label(text=f"Error: {_last_execution['error_summary']}", icon="ERROR")
1390
-
1391
-
1392
- class MCP_OT_StartServer(bpy.types.Operator):
1393
- bl_idname = "mcp.start_server"
1394
- bl_label = "Start MCP Server"
1395
-
1396
- def execute(self, context):
1397
- global _server
1398
- if _server is None:
1399
- _server = BlenderMCPServer()
1400
- _server.start()
1401
- self.report({"INFO"}, f"MCP Bridge started on {_server._host}:{_server._port}")
1402
- return {"FINISHED"}
1403
-
1404
-
1405
- class MCP_OT_StopServer(bpy.types.Operator):
1406
- bl_idname = "mcp.stop_server"
1407
- bl_label = "Stop MCP Server"
1408
-
1409
- def execute(self, context):
1410
- global _server
1411
- if _server:
1412
- _server.stop()
1413
- self.report({"INFO"}, "MCP Bridge stopped")
1414
- return {"FINISHED"}
1415
-
1416
-
1417
- classes = (MCP_AddonPreferences, MCP_PT_Panel, MCP_OT_StartServer, MCP_OT_StopServer)
1418
-
1419
-
1420
- def register():
1421
- for cls in classes:
1422
- bpy.utils.register_class(cls)
1423
- if _on_load_post not in bpy.app.handlers.load_post:
1424
- bpy.app.handlers.load_post.append(_on_load_post)
1425
- _ensure_server_running()
1426
-
1427
-
1428
- def unregister():
1429
- global _server
1430
- if _on_load_post in bpy.app.handlers.load_post:
1431
- bpy.app.handlers.load_post.remove(_on_load_post)
1432
- if _server:
1433
- _server.stop()
1434
- _server = None
1435
- for cls in reversed(classes):
1436
- bpy.utils.unregister_class(cls)
1437
-
1438
-
1439
- if __name__ == "__main__":
1440
- register()