@hybridlabor-api/aos 4.4.0 → 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 (306) 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/hooks/memb-inject.mjs +6 -0
  20. package/CLAUDE.md +573 -0
  21. package/README.de.md +0 -8
  22. package/README.md +0 -8
  23. package/README.pt.md +0 -8
  24. package/assets/brand/ao-ant-mark.svg +91 -0
  25. package/assets/brand/bdb-core-mark.svg +34 -0
  26. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  27. package/assets/brand/memb-crystal-mark.svg +18 -0
  28. package/bin/aos-config.mjs +89 -0
  29. package/bin/aos-dashboard.mjs +608 -0
  30. package/bin/aos-uninstall.mjs +227 -0
  31. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  32. package/docs/skills_table.md +0 -3
  33. package/installer.js +428 -237
  34. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  35. package/mcps/after-effects-mcp/build/index.js +840 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  37. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  38. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  39. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  40. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  41. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  42. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  43. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  44. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  45. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  46. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  47. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  48. package/mcps/computer-use-mcp/dist/client.js +136 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  50. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  51. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  52. package/mcps/computer-use-mcp/dist/native.js +50 -0
  53. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  54. package/mcps/computer-use-mcp/dist/server.js +342 -0
  55. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  56. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  57. package/package.json +13 -4
  58. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  59. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  60. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  61. package/skills/global_config/aos-setup/SKILL.md +110 -11
  62. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +48 -4
  63. package/skills/global_config/ask-tim/SKILL.md +2 -7
  64. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  65. package/bin/setup-saas.mjs +0 -792
  66. package/mcps/blender-mcp-server/.dockerignore +0 -19
  67. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  68. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  69. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  70. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  71. package/mcps/blender-mcp-server/Dockerfile +0 -19
  72. package/mcps/blender-mcp-server/LICENSE +0 -21
  73. package/mcps/blender-mcp-server/README.md +0 -409
  74. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  75. package/mcps/blender-mcp-server/addon/models.py +0 -223
  76. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  77. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  78. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  79. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  80. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  81. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  82. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  83. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  85. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  86. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  87. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  88. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  89. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  90. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  91. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  92. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  93. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  94. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  95. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  96. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  97. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  98. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  99. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  100. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  101. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  104. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  105. package/mcps/davinci-mcp-professional/.bandit +0 -35
  106. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  107. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  108. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  109. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  110. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  112. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  113. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  114. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  115. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  116. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  117. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  118. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  119. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  120. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  121. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  122. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  123. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  124. package/mcps/davinci-mcp-professional/README.md +0 -143
  125. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  126. package/mcps/davinci-mcp-professional/USING.md +0 -301
  127. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  128. package/mcps/davinci-mcp-professional/build.py +0 -169
  129. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  130. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  131. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  132. package/mcps/davinci-mcp-professional/main.py +0 -40
  133. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  134. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  135. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  144. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  145. package/mcps/davinci-mcp-professional/test.py +0 -186
  146. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  147. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  148. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  149. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  151. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  152. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  153. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  154. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  155. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  156. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  157. package/mcps/davinci-resolve-mcp/README.md +0 -173
  158. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  159. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  160. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  161. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  162. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  163. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  164. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  165. package/mcps/davinci-resolve-mcp/package.json +0 -52
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  169. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  170. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  171. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  172. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  173. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  174. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  176. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  177. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  178. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  179. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  180. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  181. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  182. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  183. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  184. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  185. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  187. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  188. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  189. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  190. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  192. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  193. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  194. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  195. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  196. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  200. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  201. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  202. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  203. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  204. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  205. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  206. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  207. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  208. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  209. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  210. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  211. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  212. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  213. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  214. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  215. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  216. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  217. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  218. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  219. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  220. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  222. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  223. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  225. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  227. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  229. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  230. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  231. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  232. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  233. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  234. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  235. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  237. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  238. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  239. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  240. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  241. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  245. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  246. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  247. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  248. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  249. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  250. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  256. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  257. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  258. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  259. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  260. package/mcps/vectorworks-mcp/README.md +0 -156
  261. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  262. package/mcps/vectorworks-mcp/app/api.py +0 -52
  263. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  264. package/mcps/vectorworks-mcp/app/config.py +0 -29
  265. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  266. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  267. package/mcps/vectorworks-mcp/app/main.py +0 -33
  268. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  269. package/mcps/vectorworks-mcp/app/search.py +0 -54
  270. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  271. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  276. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  277. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  279. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  280. package/mcps/vectorworks-mcp/design.md +0 -57
  281. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  282. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  283. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  284. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  285. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  286. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  287. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  288. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  289. package/scripts/ecosystem-health-audit.js +0 -257
  290. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  291. package/skills/bdb-dev-os-skill/package.json +0 -11
  292. package/skills/bdbsaastraining/SKILL.md +0 -316
  293. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  294. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  295. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  296. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  297. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  298. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  299. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  300. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  301. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  302. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  303. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  304. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  305. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  306. 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()