meltygui 0.1.0__py3-none-any.whl

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 (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,657 @@
1
+ """In-process MCP server for the launcher (model_server).
2
+
3
+ The launcher process is the long-lived supervisor: it stays up across studio
4
+ sessions (and is itself kept alive by the run loop). Hosting the MCP server here
5
+ — rather than in the short-lived Melty studio session — means it's reachable
6
+ whenever the launcher is, with no process killing and no race with the run loop.
7
+
8
+ Two entry points, both called from ``model_server.py``'s ``__main__``:
9
+
10
+ * ``install_log_tee()`` — tee stdout/stderr to a logfile so ``get_logs`` can
11
+ read the console across studio open/close.
12
+ * ``start_launcher_mcp(model_server)`` — run a FastMCP streamable-http server in
13
+ a daemon thread, exposing tools that drive the launcher in-place:
14
+ ``get_logs``, ``status``, ``launch``, ``restart``.
15
+
16
+ Tools call thread-safe methods on the ModelServer (``mcp_launch`` etc.), which
17
+ queue work on the existing task queue — the same path as the Ctrl+Enter re-run.
18
+ """
19
+
20
+ import functools
21
+ import logging
22
+ import re
23
+ import sys
24
+ import threading
25
+ import time
26
+ import traceback
27
+ from pathlib import Path
28
+
29
+ # Project root: src/lsd/gl_gui/mcp_server.py -> parents[3] == latent-descent/
30
+ from meltygui.core.runtime.paths import cache_root
31
+ _ROOT = cache_root()
32
+ STATE_DIR = _ROOT
33
+ LOG_PATH = STATE_DIR / "console.log"
34
+
35
+ import os
36
+
37
+ from meltygui.core.rendering.window_decoration import window
38
+
39
+ PORT = int(os.environ.get("MELTY_MCP_PORT", "8787"))
40
+ HOST = "127.0.0.1"
41
+
42
+ _tee_installed = False
43
+ _mcp_started = False
44
+
45
+ # The uvicorn server + its event loop, captured in _run so the Melty lifecycle
46
+ # handlers can reach in and drop connections without stopping the listener (the
47
+ # launcher can stay reachable across studio sessions, e.g. for `launch`/`status`
48
+ # while idle).
49
+ _uvicorn_server = None
50
+ _uvicorn_loop = None
51
+ # Set while Melty is tearing down: the gate closes during the drop so a fresh
52
+ # request can't re-hang the launcher. Auto-clears shortly after the drop (see
53
+ # notify_melty_shutdown), so the launcher is reachable again while idle.
54
+ _draining = threading.Event()
55
+ # Drop requests until Melty has painted this many frames - see _serving_ready.
56
+ WARMUP_FRAMES = 3
57
+
58
+ # Cap stored result/error text so a chatty tool (get_logs returning 200 lines)
59
+ # can't balloon the in-memory log.
60
+ _LOG_RESULT_CAP = 2000
61
+
62
+ _ANSI = re.compile(r"\x1b\[[0-9;]*m")
63
+ # Markers that begin an error region, in this codebase's three formats:
64
+ # - "Exception Trace": print_stack_trace(exception=...) - a ─-bar-delimited
65
+ # block with frames, variable-watch tables, and a final "ExcType: msg".
66
+ # - "Traceback (most recent call last):": standard (thread handlers, log_exc).
67
+ # - "Error Caught": the older print_colored_traceback banner.
68
+ _PST_TITLE = "Exception Trace"
69
+ _FALLBACK_MARKERS = ("Traceback (most recent call last):", "Error Caught")
70
+
71
+
72
+ def _is_bar(line):
73
+ s = line.strip()
74
+ return len(s) >= 20 and set(s) == {"─"}
75
+
76
+
77
+ def _extract_last_error(text, cap=200):
78
+ """Return the most recent error block from `text` (ANSI stripped), or None.
79
+
80
+ Picks whichever error format appears latest in the log.
81
+ """
82
+ lines = [_ANSI.sub("", l) for l in text.splitlines()]
83
+
84
+ # Latest marker of any format.
85
+ pst = std = None
86
+ for i in range(len(lines) - 1, -1, -1):
87
+ if pst is None and _PST_TITLE in lines[i]:
88
+ pst = i
89
+ if std is None and any(mk in lines[i] for mk in _FALLBACK_MARKERS):
90
+ std = i
91
+ if pst is not None and std is not None:
92
+ break
93
+ if pst is None and std is None:
94
+ return None
95
+
96
+ # print_stack_trace block: from the ─-bar above the title to the closing bar.
97
+ if pst is not None and (std is None or pst > std):
98
+ bars = [i for i, l in enumerate(lines) if _is_bar(l)]
99
+ before = [b for b in bars if b < pst]
100
+ after = [b for b in bars if b > pst]
101
+ b0 = before[-1] if before else pst
102
+ # after[0] is the bar right after the title; after[1] is the closing bar.
103
+ b2 = after[1] if len(after) >= 2 else (after[0] if after else len(lines) - 1)
104
+ block = lines[b0:b2 + 1]
105
+ return "\n".join(block[:cap])
106
+
107
+ # Fallback: cut from the marker to its exception message line.
108
+ block, seen_frame, j = [], False, std
109
+ while j < len(lines) and len(block) < cap:
110
+ l = lines[j]
111
+ s = l.strip()
112
+ is_marker = any(mk in l for mk in _FALLBACK_MARKERS)
113
+ is_file = s.startswith('File "')
114
+ is_indented = l[:1] in (" ", "\t")
115
+ if is_marker or is_file or is_indented or s == "":
116
+ block.append(l)
117
+ if is_file or is_indented:
118
+ seen_frame = True
119
+ j += 1
120
+ continue
121
+ block.append(l) # the message line
122
+ if seen_frame:
123
+ break
124
+ j += 1
125
+ while block and not block[-1].strip():
126
+ block.pop()
127
+ return "\n".join(block) if block else None
128
+
129
+
130
+ class _Tee:
131
+ """Write to the real stream and the logfile at once. Thread-safe.
132
+
133
+ Falls back gracefully if either sink raises so logging can never take the
134
+ process down.
135
+ """
136
+
137
+ def __init__(self, stream, fh, lock):
138
+ self._stream = stream
139
+ self._fh = fh
140
+ self._lock = lock
141
+
142
+ def write(self, data):
143
+ with self._lock:
144
+ try:
145
+ self._stream.write(data)
146
+ except Exception:
147
+ pass
148
+ try:
149
+ self._fh.write(data)
150
+ self._fh.flush()
151
+ except Exception:
152
+ pass
153
+
154
+ def flush(self):
155
+ try:
156
+ self._stream.flush()
157
+ except Exception:
158
+ pass
159
+ try:
160
+ self._fh.flush()
161
+ except Exception:
162
+ pass
163
+
164
+ def __getattr__(self, name):
165
+ return getattr(self._stream, name)
166
+
167
+
168
+ class _HttpRequestLine(logging.Handler):
169
+ """Prints httpx's per-response INFO record as ONE short console line:
170
+
171
+ [http] 20:26:36 GET localhost:11434/api/tags 200
172
+
173
+ httpx logs `'HTTP Request: %s %s "%s %d %s"'` with args (method, url,
174
+ http_version, status, reason); the scheme is dropped and the reason kept
175
+ only for non-2xx/3xx answers (`… 429 Too Many Requests`). Any other record
176
+ on the logger falls back to its plain message. Goes through print() so
177
+ the log tee mirrors it into console.log like every other line.
178
+ """
179
+
180
+ def emit(self, record):
181
+ try:
182
+ args = record.args if isinstance(record.args, tuple) else ()
183
+ if len(args) == 5 and str(record.msg).startswith("HTTP Request:"):
184
+ method, url, _version, status, reason = args
185
+ url = re.sub(r"^https?://", "", str(url))
186
+ status = int(status)
187
+ tail = f"{status}" if status < 400 else f"{status} {reason}"
188
+ text = f"{method} {url} {tail}"
189
+ else:
190
+ text = record.getMessage()
191
+ print(f"[http] {time.strftime('%H:%M:%S')} {text}")
192
+ except Exception:
193
+ pass
194
+
195
+
196
+ _http_logging_installed = False
197
+
198
+
199
+ def install_concise_http_logging():
200
+ """Route the `httpx` logger to _HttpRequestLine and stop it propagating,
201
+ so the request lines never reach whatever handler sits on the root.
202
+ Idempotent."""
203
+ global _http_logging_installed
204
+ if _http_logging_installed:
205
+ return
206
+ _http_logging_installed = True
207
+ http_logger = logging.getLogger("httpx")
208
+ http_logger.setLevel(logging.INFO)
209
+ http_logger.propagate = False
210
+ http_logger.addHandler(_HttpRequestLine())
211
+
212
+
213
+ def install_log_tee():
214
+ """Mirror stdout/stderr into LOG_PATH (fresh per process). Idempotent."""
215
+ global _tee_installed
216
+ if _tee_installed:
217
+ return
218
+ _tee_installed = True
219
+ try:
220
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
221
+ fh = open(LOG_PATH, "w", buffering=1)
222
+ lock = threading.Lock()
223
+ sys.stdout = _Tee(sys.__stdout__, fh, lock)
224
+ sys.stderr = _Tee(sys.__stderr__, fh, lock)
225
+ except Exception as e:
226
+ print(f"[mcp] could not install log tee: {e}")
227
+
228
+
229
+ # --- MCP activity toasts ----------------------------------------------------
230
+ # Every tool call pops a toast in the studio (tag "MCP") so Claude and
231
+ # the live process is visible. Color-coded per tool, red on error. notify() fires
232
+ # UNCONDITIONALLY on every call: it's thread-safe, no-op in its render wake when
233
+ # no window exists, and an idle-time call just appends to the bounded deque
234
+ # (maxlen 20) and shows on the next session - no studio gate.
235
+ MCP_TOOL_TINTS = {
236
+ "get_logs": (0.55, 0.70, 0.95, 1.0),
237
+ "status": (0.45, 0.80, 1.00, 1.0),
238
+ "last_error": (1.00, 0.65, 0.30, 1.0),
239
+ "launch": (0.40, 1.00, 0.55, 1.0),
240
+ "restart": (1.00, 0.80, 0.35, 1.0),
241
+ "restart_launcher": (1.00, 0.55, 0.40, 1.0),
242
+ "hotswap": (0.55, 0.90, 1.00, 1.0),
243
+ "recompile_external_changes": (0.55, 1.00, 0.75, 1.0),
244
+ "screenshot": (0.80, 0.60, 1.00, 1.0),
245
+ "list_windows": (0.70, 0.75, 0.85, 1.0),
246
+ "eval_python": (0.45, 0.95, 0.80, 1.0),
247
+ "find_views": (0.60, 0.85, 0.95, 1.0),
248
+ "describe_view": (0.60, 0.85, 0.95, 1.0),
249
+ "hit_test": (0.95, 0.75, 0.55, 1.0),
250
+ "param_sources": (0.85, 0.70, 0.95, 1.0),
251
+ "tile_cache": (0.70, 0.95, 0.60, 1.0),
252
+ }
253
+ MCP_DEFAULT_TINT = (0.70, 0.72, 0.82, 1.0)
254
+ MCP_ERROR_TINT = (1.00, 0.35, 0.35, 1.0)
255
+
256
+
257
+ def _mcp_toast_text(name, call_args):
258
+ """Compact one-line summary of a tool call for the notification toast:
259
+ the tool name plus its args, each value collapsed to a single line and
260
+ truncated so a big `source`/`code` payload can't blow up the toast."""
261
+ def short(v):
262
+ s = " ".join(str(v).split())
263
+ return s if len(s) <= 40 else s[:40] + "..."
264
+ detail = ", ".join(f"{k}={short(v)}" for k, v in call_args.items())
265
+ return f"{name}({detail})" if detail else f"{name}()"
266
+
267
+
268
+ def start_launcher_mcp(model_server, host=HOST, port=PORT):
269
+ """Start the launcher MCP server in a daemon thread. Idempotent."""
270
+ global _mcp_started
271
+ if _mcp_started:
272
+ return
273
+ _mcp_started = True
274
+
275
+ install_log_tee()
276
+
277
+ try:
278
+ import asyncio
279
+ import uvicorn
280
+ from mcp.server.fastmcp import FastMCP, Image
281
+ except Exception as e:
282
+ print(f"[mcp] not starting — dependencies unavailable: {e}")
283
+ return
284
+
285
+ # Keep MCP/uvicorn/etc logging out of the launcher console (and the
286
+ # tee'd logfile that get_logs reads).
287
+ for _name in ("uvicorn", "uvicorn.error", "uvicorn.access",
288
+ "mcp", "mcp.server", "sse_starlette"):
289
+ logging.getLogger(_name).setLevel(logging.WARNING)
290
+
291
+ # FastMCP's constructor calls logging.basicConfig(level=INFO) with a rich
292
+ # handler on the ROOT logger, which then rendered every library INFO
293
+ # record - httpx's `HTTP Request: GET http://... "HTTP/1.1 200 OK"` for each
294
+ # Ollama probe / Anthropic call - as a wide rich line with file-link
295
+ # escapes, or one character per line when it misjudged the tee's width.
296
+ # Snapshot the root logger before the constructor and set it back, then
297
+ # give httpx its own one-line handler (install_concise_http_logging).
298
+ root_logger = logging.getLogger()
299
+ root_handlers, root_level = list(root_logger.handlers), root_logger.level
300
+ mcp = FastMCP("meltygui", host=host, port=port)
301
+ for handler in list(root_logger.handlers):
302
+ if handler not in root_handlers:
303
+ root_logger.removeHandler(handler)
304
+ root_logger.setLevel(root_level)
305
+ install_concise_http_logging()
306
+
307
+ def logged_tool():
308
+ """Like ``mcp.tool()`` but records each call to ``MCPServerLog.logs``.
309
+
310
+ ``functools.wraps`` copies ``__wrapped__`` so FastMCP's
311
+ ``inspect.signature`` still resolves the original signature, name, and
312
+ docstring — the tool schema is unchanged.
313
+ """
314
+
315
+ def deco(fn):
316
+ @functools.wraps(fn)
317
+ def wrapper(*args, **kwargs):
318
+ # Resolve notify/MCPServerLog PER CALL, not at registration. The
319
+ # tool wrappers are registered once at launcher boot; a later
320
+ # hotswap of notifications.py / monitor.py re-imports the module and
321
+ # makes a NEW NotificationCenter class (with fresh deques) that the
322
+ # render loop reads. A registered-level `import` would keep
323
+ # appending to the OLD, pre-hotswap class - toasts land in an
324
+ # orphaned deque nothing draws (the post-hotswap silent-toast bug).
325
+ # Re-importing here always hits the live sys.modules entry.
326
+ from meltygui.core.diagnostics.monitor_core import MCPServerLog
327
+ from meltygui.core.diagnostics.notifications import notify
328
+ # Bind positional args to names so the log is self-describing.
329
+ call_args = dict(kwargs)
330
+ names = list(fn.__code__.co_varnames[:fn.__code__.co_argcount])
331
+ for name, val in zip(names, args):
332
+ call_args[name] = val
333
+ try:
334
+ result = fn(*args, **kwargs)
335
+ except Exception:
336
+ MCPServerLog.record(fn.__name__, call_args,
337
+ error=traceback.format_exc())
338
+ notify(_mcp_toast_text(fn.__name__, call_args) + " (error)",
339
+ tint=MCP_ERROR_TINT, tag="MCP")
340
+ raise
341
+ MCPServerLog.record(fn.__name__, call_args, result=result)
342
+ notify(_mcp_toast_text(fn.__name__, call_args),
343
+ tint=MCP_TOOL_TINTS.get(fn.__name__, MCP_DEFAULT_TINT),
344
+ tag="MCP")
345
+ return result
346
+
347
+ return mcp.tool()(wrapper)
348
+
349
+ return deco
350
+
351
+ @logged_tool()
352
+ def get_logs(lines: int = 200) -> str:
353
+ """Return the last `lines` lines of the launcher's console output.
354
+
355
+ Captures stdout + stderr for the launcher and any studio session it
356
+ runs. Use this to read tracebacks, training progress, or warnings.
357
+ """
358
+ if not LOG_PATH.exists():
359
+ return "(no log file yet)"
360
+ rows = LOG_PATH.read_text(errors="replace").splitlines()
361
+ if not rows:
362
+ return "(log is empty)"
363
+ return "\n".join(rows[-max(1, lines):])
364
+
365
+ @logged_tool()
366
+ def status() -> str:
367
+ """Report the launcher PID, whether a studio session is running, and —
368
+ if it isn't — why the last session stopped (user_quit / restart / crash).
369
+
370
+ Use the reason to tell an expected exit (the user closed or restarted the
371
+ window) from a crash worth investigating, instead of treating every idle
372
+ studio as a bug.
373
+ """
374
+ import meltygui.core.diagnostics.session_status as session_status
375
+ return f"{model_server.mcp_status()}; {session_status.summary()}"
376
+
377
+ @logged_tool()
378
+ def last_error() -> str:
379
+ """Return the most recent traceback from the console log, or a note that
380
+ the run looks clean. Faster than scanning get_logs after a crash."""
381
+ if not LOG_PATH.exists():
382
+ return "(no log file yet)"
383
+ block = _extract_last_error(LOG_PATH.read_text(errors="replace"))
384
+ if block is None:
385
+ return "no traceback found in the current run's log (looks clean)"
386
+ return block
387
+
388
+ @logged_tool()
389
+ def launch() -> str:
390
+ """Open the latent-descent studio by replaying the last run.
391
+
392
+ No-op (with a message) if a studio session is already running.
393
+ """
394
+ return model_server.mcp_launch()
395
+
396
+ @logged_tool()
397
+ def restart() -> str:
398
+ """Restart the studio session in place: interrupt the running session
399
+ (if any) and replay the last run. The launcher process stays up, so the
400
+ MCP connection survives — just call get_logs afterward.
401
+ """
402
+ return model_server.mcp_restart()
403
+
404
+ @logged_tool()
405
+ def hotswap(path: str, source: str = "") -> str:
406
+ """Recompile a project source file and hotswap it into the running studio
407
+ process — apply code changes live, no full restart.
408
+
409
+ path: absolute or project-relative path to a .py file whose module is
410
+ already imported in the running process.
411
+ source: optional full new file contents. Omit it to reload the file's
412
+ current on-disk contents (e.g. after editing it on disk). If
413
+ given, it is hotswapped first and written to disk only on a clean
414
+ compile, so a syntax error never leaves broken code on disk.
415
+
416
+ The whole module is reloaded: every function/class in the file is patched
417
+ in place, so imported names and live instances keep working. A swap that
418
+ compiles but throws at runtime is auto-reverted by the editor's hotswap
419
+ guard. Library/stdlib paths are refused. Returns a status line.
420
+ """
421
+ from meltygui.core.diagnostics.notifications import notify
422
+ notify(f"hotswap requested: {path}", tint=MCP_TOOL_TINTS.get("hotswap", MCP_DEFAULT_TINT), tag="MCP")
423
+ import meltygui.core.automation.mcp_hotswap as mcp_hotswap
424
+ return mcp_hotswap.hotswap_file(path, source or None)
425
+
426
+ @logged_tool()
427
+ def recompile_external_changes() -> str:
428
+ """Recompile everything the studio has queued — pending edits AND
429
+ tracked external changes — the SAME code path as clicking the Pending
430
+ Saves window's recompile button (PendingSave.recompile_all_ui drives
431
+ the button's own runner draw_state: busy spinner while running, then
432
+ the fading check mark + summary), so the button and this tool always
433
+ behave identically. The window is revealed so the result is visible.
434
+
435
+ External changes are first decomposed into per-span pending entries
436
+ and merged with any overlapping pending edits — rebase / per-span
437
+ 3-way merge / adopt (MERGED / ADOPTED / CONFLICT lines land in the
438
+ window's persistent merge display); the external window keeps showing
439
+ absorbed drift until the user dismisses it. Each entry then hotswaps
440
+ in place with hotswap-guard rollback. Returns the same summary string
441
+ the button shows.
442
+ """
443
+ from meltygui.core.diagnostics.notifications import notify
444
+ notify("recompile requested",
445
+ tint=MCP_TOOL_TINTS.get("recompile_external_changes", MCP_DEFAULT_TINT), tag="MCP")
446
+ from meltygui.editor.pending_save import PendingSave
447
+ return PendingSave.recompile_all_ui()
448
+
449
+ @logged_tool()
450
+ def screenshot(window: str):
451
+ """Capture one Melty studio window by name and return it as a PNG image.
452
+
453
+ Pass the window's title (exact, else case-insensitive substring). Use
454
+ list_windows to see what's open. Captures a single window rather than
455
+ the whole display, which may span an ultra-wide monitor.
456
+ """
457
+ from meltygui.core.diagnostics.notifications import notify
458
+ notify(f"screenshot requested: {window}", tint=MCP_TOOL_TINTS.get("screenshot", MCP_DEFAULT_TINT), tag="MCP")
459
+ if not model_server._studio_running():
460
+ return "no studio session running — call launch first"
461
+ from meltygui.core.graphics.screenshot import request_capture
462
+ path, error = request_capture(window)
463
+ if error:
464
+ return f"screenshot failed: {error}"
465
+ return Image(path=path)
466
+
467
+ @logged_tool()
468
+ def list_windows() -> str:
469
+ """List the names of currently-open Melty studio windows."""
470
+ if not model_server._studio_running():
471
+ return "no studio session running — call launch first"
472
+ from meltygui.core.graphics.screenshot import list_window_names
473
+ names = list_window_names()
474
+ return "\n".join(sorted(set(names))) if names else "(no named windows open)"
475
+
476
+ @logged_tool()
477
+ def eval_python(code: str) -> str:
478
+ """Execute Python in the live launcher process; returns stdout + result.
479
+
480
+ Runs on the studio render thread when a session is running (safe to
481
+ read/poke Melty + imgui state), else inline on the launcher thread.
482
+ In scope: `Melty`, `server`/`model_server`, and (when a studio is up)
483
+ `vis` (the studio) and `app`/`root` (the root AppModel). Import anything
484
+ else. A trailing expression's repr is returned. Arbitrary in-process
485
+ code — for inspecting/poking live state while iterating.
486
+ """
487
+ from meltygui.core.automation.mcp_eval import request_eval
488
+ return request_eval(code, model_server)
489
+
490
+ # --- Typed state queries (mcp_query.py): JSON read on the render thread ---
491
+
492
+ def _query(collect):
493
+ if not model_server._studio_running():
494
+ return "no studio session running — call launch first"
495
+ from meltygui.core.automation.mcp_query import run_query
496
+ return run_query(collect, model_server)
497
+
498
+ @logged_tool()
499
+ def find_views(func: str = "", name: str = "", window: str = "",
500
+ include_closed: bool = False, limit: int = 50) -> str:
501
+ """Find live Melty views (draw_states) by case-insensitive substring:
502
+ `func` on the render function's qualname, `name` on the view name /
503
+ tile_id, `window` on the ROOT window title (see list_windows). Rows
504
+ are front-most first with rect, clip, closed / hidden / hovered flags,
505
+ layer, z_pos, parent window and the input value's type. Use the
506
+ returned `tile_id` with describe_view / param_sources / tile_cache."""
507
+ from meltygui.core.automation.mcp_query import collect_find_views
508
+ return _query(lambda: collect_find_views(func, name, window, include_closed, limit))
509
+
510
+ @logged_tool()
511
+ def describe_view(view: str, children_depth: int = 1) -> str:
512
+ """One view in full: summary, resolved kwargs, diverged auto_params,
513
+ event_rect scopes, this frame's event subscriptions and cursor
514
+ registration, its tile-cache entry (dirty, last clean / invalidated
515
+ frame, last bump reason), window_pos / content size / scroll, the
516
+ render-tree ancestors and window chain, and children to
517
+ `children_depth`. `view` = a tile_id (exact or unique substring) or
518
+ a draw_state id prefix."""
519
+ from meltygui.core.automation.mcp_query import collect_describe_view
520
+ return _query(lambda: collect_describe_view(view, children_depth))
521
+
522
+ @logged_tool()
523
+ def hit_test(x: float, y: float) -> str:
524
+ """The BVH stack at screen point (x, y), front to back, each view
525
+ with its z_pos / priority and the event subscriptions + cursor shape
526
+ registered for it. Subscriptions exist only for views under the REAL
527
+ pointer (`pointer`, `pointer_matches_point`); elsewhere the stack is
528
+ exact but subscriptions are empty. Also: Melty.hovered_ds, the
529
+ resolved cursor shape, drag capture and blocker views."""
530
+ from meltygui.core.automation.mcp_query import collect_hit_test
531
+ return _query(lambda: collect_hit_test(x, y))
532
+
533
+ @logged_tool()
534
+ def param_sources(view: str, param: str = "") -> str:
535
+ """The context menu's inputs tab as data: for each parameter of the
536
+ view (or just `param`) the value it reads, the DRIVING source (the
537
+ SourcePriority pick) and every source that sets it in priority order
538
+ (kind, writable, value). `sources` lists the sources with file:line."""
539
+ from meltygui.core.automation.mcp_query import collect_param_sources
540
+ return _query(lambda: collect_param_sources(view, param))
541
+
542
+ @logged_tool()
543
+ def tile_cache(view: str = "", history_frames: int = 0, limit: int = 100) -> str:
544
+ """Blit tile-cache state. With `view`: that tile (dirty, clean /
545
+ invalidated cache frames, last bump, blit_served_frame, tracker note)
546
+ and its invalidations over the last `history_frames` frames. Without:
547
+ tile totals, per-frame body_runs / cache_hits / captures (last 10
548
+ frames, or `history_frames`), the latest `limit` invalidations and the
549
+ top invalidators over the window — a per-frame invalidator shows up
550
+ here with a count near the frame count."""
551
+ from meltygui.core.automation.mcp_query import collect_tile_cache
552
+ return _query(lambda: collect_tile_cache(view, history_frames, limit))
553
+
554
+ @logged_tool()
555
+ def restart_launcher() -> str:
556
+ """Fully restart the launcher process (not just the studio session).
557
+
558
+ Use this to pick up newly-added MCP tools or changes to launcher/MCP
559
+ startup code — an in-place `restart` only replays the studio and can't
560
+ register new tools. The run loop relaunches the process; reconnect after
561
+ ~15-20s. For ordinary rendering/converter code edits, prefer `restart`.
562
+ """
563
+ return model_server.mcp_restart_launcher()
564
+
565
+ def _run():
566
+ global _uvicorn_server, _uvicorn_loop
567
+ try:
568
+ loop = asyncio.new_event_loop()
569
+ asyncio.set_event_loop(loop)
570
+ app = _make_gate(mcp.streamable_http_app())
571
+ # timeout_graceful_shutdown=0: if the server ever does stop, never
572
+ # wait on connections (we drop them explicitly via notify_melty_shutdown).
573
+ config = uvicorn.Config(app, host=host, port=port, log_level="warning",
574
+ timeout_graceful_shutdown=0)
575
+ server = uvicorn.Server(config)
576
+ server.install_signal_handlers = lambda: None # off the main thread
577
+ _uvicorn_server = server
578
+ _uvicorn_loop = loop
579
+ loop.run_until_complete(server.serve())
580
+ except Exception as e:
581
+ print(f"[mcp] server thread crashed: {e}")
582
+
583
+ threading.Thread(target=_run, daemon=True, name="launcher-mcp").start()
584
+ print(f"[mcp] launcher MCP listening on http://{host}:{port}/mcp")
585
+
586
+
587
+ def _serving_ready():
588
+ """True when the server should accept requests.
589
+
590
+ Rejects during a Melty teardown drain, and until Melty has painted
591
+ WARMUP_FRAMES frames (Melty.init_complete() == frame_count > 2) so clients
592
+ can't poke a process whose GUI hasn't initialized. Fails OPEN if Melty isn't
593
+ importable yet — the launcher tools (status/launch) must stay reachable to
594
+ bring the studio up.
595
+ """
596
+ if _draining.is_set():
597
+ return False
598
+ try:
599
+ from meltygui.core.melty import Melty
600
+ return Melty.init_complete()
601
+ except Exception:
602
+ return True
603
+
604
+
605
+ def _make_gate(app):
606
+ """ASGI wrapper rejecting HTTP requests with 503 until _serving_ready().
607
+
608
+ Non-HTTP scopes (lifespan) pass through untouched so uvicorn startup/shutdown
609
+ events still fire.
610
+ """
611
+ async def gate(scope, receive, send):
612
+ if scope.get("type") == "http" and not _serving_ready():
613
+ await send({
614
+ "type": "http.response.start",
615
+ "status": 503,
616
+ "headers": [(b"content-type", b"text/plain; charset=utf-8"),
617
+ (b"connection", b"close")],
618
+ })
619
+ await send({"type": "http.response.body", "body": b"meltygui not ready"})
620
+ return
621
+ await app(scope, receive, send)
622
+
623
+ return gate
624
+
625
+
626
+ def notify_melty_shutdown():
627
+ """Drop all active MCP connections immediately, so a hanging client can't
628
+
629
+ block Melty's teardown. Keeps the listener bound (the launcher stays
630
+ reachable for the next session / idle `launch`), and clears the drain shortly
631
+ after so new requests are served again. Safe to call if the server never
632
+ started.
633
+ """
634
+ server, loop = _uvicorn_server, _uvicorn_loop
635
+ if server is None or loop is None or loop.is_closed():
636
+ return
637
+ _draining.set()
638
+
639
+ def _drop():
640
+ try:
641
+ # Same primitives uvicorn's own Server.shutdown uses: ask every live
642
+ # connection to close, and cancel any in-flight request task that
643
+ # would otherwise keep the teardown waiting.
644
+ for conn in list(getattr(server.server_state, "connections", ())):
645
+ conn.shutdown()
646
+ for task in list(getattr(server.server_state, "tasks", ())):
647
+ task.cancel()
648
+ except Exception as e:
649
+ print(f"[mcp] error dropping connections on meltygui shutdown: {e}")
650
+ finally:
651
+ loop.call_later(1.0, _draining.clear)
652
+
653
+ try:
654
+ loop.call_soon_threadsafe(_drop)
655
+ except RuntimeError:
656
+ # Loop already gone - nothing to drop.
657
+ _draining.clear()