livepilot 1.10.5 → 1.10.7

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 (111) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.mcp.json.disabled +9 -0
  3. package/.mcpbignore +3 -0
  4. package/AGENTS.md +3 -3
  5. package/BUGS.md +1570 -0
  6. package/CHANGELOG.md +92 -0
  7. package/CONTRIBUTING.md +1 -1
  8. package/README.md +7 -7
  9. package/bin/livepilot.js +28 -8
  10. package/livepilot/.Codex-plugin/plugin.json +2 -2
  11. package/livepilot/.claude-plugin/plugin.json +2 -2
  12. package/livepilot/skills/livepilot-core/SKILL.md +4 -4
  13. package/livepilot/skills/livepilot-core/references/overview.md +2 -2
  14. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  15. package/livepilot/skills/livepilot-release/SKILL.md +8 -8
  16. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  17. package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
  18. package/m4l_device/LivePilot_Analyzer.maxproj +53 -0
  19. package/m4l_device/livepilot_bridge.js +226 -3
  20. package/manifest.json +3 -3
  21. package/mcp_server/__init__.py +1 -1
  22. package/mcp_server/atlas/__init__.py +93 -26
  23. package/mcp_server/composer/sample_resolver.py +10 -6
  24. package/mcp_server/composer/tools.py +10 -6
  25. package/mcp_server/connection.py +6 -1
  26. package/mcp_server/creative_constraints/tools.py +214 -40
  27. package/mcp_server/experiment/engine.py +16 -14
  28. package/mcp_server/experiment/tools.py +9 -9
  29. package/mcp_server/hook_hunter/analyzer.py +62 -9
  30. package/mcp_server/hook_hunter/tools.py +74 -18
  31. package/mcp_server/m4l_bridge.py +32 -6
  32. package/mcp_server/memory/taste_graph.py +7 -2
  33. package/mcp_server/mix_engine/tools.py +8 -3
  34. package/mcp_server/musical_intelligence/detectors.py +32 -0
  35. package/mcp_server/musical_intelligence/tools.py +15 -10
  36. package/mcp_server/performance_engine/tools.py +117 -30
  37. package/mcp_server/preview_studio/engine.py +89 -8
  38. package/mcp_server/preview_studio/tools.py +43 -21
  39. package/mcp_server/project_brain/automation_graph.py +71 -19
  40. package/mcp_server/project_brain/builder.py +2 -0
  41. package/mcp_server/project_brain/tools.py +73 -15
  42. package/mcp_server/reference_engine/profile_builder.py +129 -3
  43. package/mcp_server/reference_engine/tools.py +54 -11
  44. package/mcp_server/runtime/capability_probe.py +10 -4
  45. package/mcp_server/runtime/execution_router.py +50 -0
  46. package/mcp_server/runtime/mcp_dispatch.py +75 -3
  47. package/mcp_server/runtime/remote_commands.py +4 -2
  48. package/mcp_server/runtime/tools.py +8 -2
  49. package/mcp_server/sample_engine/analyzer.py +131 -4
  50. package/mcp_server/sample_engine/critics.py +29 -8
  51. package/mcp_server/sample_engine/models.py +20 -1
  52. package/mcp_server/sample_engine/tools.py +74 -31
  53. package/mcp_server/semantic_moves/sound_design_compilers.py +22 -59
  54. package/mcp_server/semantic_moves/tools.py +5 -1
  55. package/mcp_server/semantic_moves/transition_compilers.py +12 -19
  56. package/mcp_server/server.py +78 -11
  57. package/mcp_server/services/motif_service.py +9 -3
  58. package/mcp_server/session_continuity/models.py +4 -0
  59. package/mcp_server/session_continuity/tools.py +7 -3
  60. package/mcp_server/session_continuity/tracker.py +23 -9
  61. package/mcp_server/song_brain/builder.py +110 -12
  62. package/mcp_server/song_brain/tools.py +94 -25
  63. package/mcp_server/sound_design/tools.py +112 -1
  64. package/mcp_server/splice_client/client.py +19 -6
  65. package/mcp_server/stuckness_detector/detector.py +90 -0
  66. package/mcp_server/stuckness_detector/tools.py +49 -5
  67. package/mcp_server/tools/_agent_os_engine/__init__.py +52 -0
  68. package/mcp_server/tools/_agent_os_engine/critics.py +158 -0
  69. package/mcp_server/tools/_agent_os_engine/evaluation.py +206 -0
  70. package/mcp_server/tools/_agent_os_engine/models.py +132 -0
  71. package/mcp_server/tools/_agent_os_engine/taste.py +192 -0
  72. package/mcp_server/tools/_agent_os_engine/techniques.py +161 -0
  73. package/mcp_server/tools/_agent_os_engine/world_model.py +170 -0
  74. package/mcp_server/tools/_composition_engine/__init__.py +67 -0
  75. package/mcp_server/tools/_composition_engine/analysis.py +174 -0
  76. package/mcp_server/tools/_composition_engine/critics.py +522 -0
  77. package/mcp_server/tools/_composition_engine/gestures.py +230 -0
  78. package/mcp_server/tools/_composition_engine/harmony.py +160 -0
  79. package/mcp_server/tools/_composition_engine/models.py +193 -0
  80. package/mcp_server/tools/_composition_engine/sections.py +414 -0
  81. package/mcp_server/tools/_harmony_engine.py +52 -8
  82. package/mcp_server/tools/_perception_engine.py +18 -11
  83. package/mcp_server/tools/_research_engine.py +98 -19
  84. package/mcp_server/tools/_theory_engine.py +138 -9
  85. package/mcp_server/tools/agent_os.py +43 -18
  86. package/mcp_server/tools/analyzer.py +105 -8
  87. package/mcp_server/tools/automation.py +6 -1
  88. package/mcp_server/tools/clips.py +45 -0
  89. package/mcp_server/tools/composition.py +90 -38
  90. package/mcp_server/tools/devices.py +32 -7
  91. package/mcp_server/tools/harmony.py +115 -14
  92. package/mcp_server/tools/midi_io.py +13 -1
  93. package/mcp_server/tools/mixing.py +35 -1
  94. package/mcp_server/tools/motif.py +56 -5
  95. package/mcp_server/tools/planner.py +6 -2
  96. package/mcp_server/tools/research.py +37 -10
  97. package/mcp_server/tools/theory.py +108 -16
  98. package/mcp_server/transition_engine/critics.py +18 -11
  99. package/mcp_server/transition_engine/tools.py +6 -1
  100. package/mcp_server/translation_engine/tools.py +8 -6
  101. package/mcp_server/wonder_mode/engine.py +8 -3
  102. package/mcp_server/wonder_mode/tools.py +29 -21
  103. package/package.json +2 -2
  104. package/remote_script/LivePilot/__init__.py +57 -2
  105. package/remote_script/LivePilot/clips.py +69 -0
  106. package/remote_script/LivePilot/mixing.py +117 -0
  107. package/remote_script/LivePilot/router.py +13 -1
  108. package/scripts/generate_tool_catalog.py +13 -38
  109. package/scripts/sync_metadata.py +231 -14
  110. package/mcp_server/tools/_agent_os_engine.py +0 -947
  111. package/mcp_server/tools/_composition_engine.py +0 -1530
@@ -7,19 +7,23 @@
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import logging
11
+
10
12
  from fastmcp import Context
11
13
 
12
14
  from ..server import mcp
13
15
  from . import engine
14
16
 
17
+ logger = logging.getLogger(__name__)
18
+
15
19
 
16
20
  def _get_song_brain_dict() -> dict:
17
21
  try:
18
22
  from ..song_brain.tools import _current_brain
19
23
  if _current_brain is not None:
20
24
  return _current_brain.to_dict()
21
- except Exception:
22
- pass
25
+ except Exception as exc:
26
+ logger.warning("song_brain lookup failed: %s", exc)
23
27
  return {}
24
28
 
25
29
 
@@ -37,8 +41,8 @@ def _get_taste_graph(ctx: Context):
37
41
  taste_store=taste_store, anti_store=anti_store,
38
42
  persistent_store=persistent,
39
43
  )
40
- except Exception:
41
- pass
44
+ except Exception as exc:
45
+ logger.warning("taste_graph build failed: %s", exc)
42
46
  return None
43
47
 
44
48
 
@@ -47,7 +51,8 @@ def _get_active_constraints():
47
51
  try:
48
52
  from ..creative_constraints.tools import _active_constraints
49
53
  return _active_constraints
50
- except Exception:
54
+ except Exception as exc:
55
+ logger.debug("creative_constraints not importable: %s", exc)
51
56
  return None
52
57
 
53
58
 
@@ -60,7 +65,8 @@ def _get_ledger_entries(ctx: Context) -> list[dict]:
60
65
  )
61
66
  entries = ledger.get_recent_moves(limit=20)
62
67
  return [e.to_dict() for e in entries]
63
- except Exception:
68
+ except Exception as exc:
69
+ logger.warning("action_ledger recent_moves failed: %s", exc)
64
70
  return []
65
71
 
66
72
 
@@ -77,15 +83,16 @@ def _get_stuckness_report(ctx: Context, song_brain: dict) -> dict | None:
77
83
  ableton = ctx.lifespan_context.get("ableton")
78
84
  if ableton:
79
85
  session_info = ableton.send_command("get_session_info", {})
80
- except Exception:
81
- pass
86
+ except Exception as exc:
87
+ logger.warning("session_info fetch for stuckness failed: %s", exc)
82
88
  report = detect_stuckness(
83
89
  action_history=action_ledger,
84
90
  session_info=session_info,
85
91
  song_brain=song_brain,
86
92
  )
87
93
  return report.to_dict()
88
- except Exception:
94
+ except Exception as exc:
95
+ logger.warning("stuckness detection failed: %s", exc)
89
96
  return None
90
97
 
91
98
 
@@ -144,8 +151,9 @@ def enter_wonder_mode(
144
151
  sample_context["sample_file_path"] = best.get("file_path", "")
145
152
  sample_context["sample_name"] = best.get("name", "")
146
153
  sample_context["material_type"] = best.get("material_type", "")
147
- except Exception:
148
- pass # Graceful degradation — analytical variants still work
154
+ except Exception as exc:
155
+ # Graceful degradation — analytical variants still work
156
+ logger.warning("sample opportunity search failed: %s", exc)
149
157
 
150
158
  # 1c. Get session info for kernel
151
159
  session_info = {}
@@ -153,8 +161,8 @@ def enter_wonder_mode(
153
161
  ableton = ctx.lifespan_context.get("ableton")
154
162
  if ableton:
155
163
  session_info = ableton.send_command("get_session_info", {})
156
- except Exception:
157
- pass
164
+ except Exception as exc:
165
+ logger.warning("session_info fetch for kernel failed: %s", exc)
158
166
 
159
167
  # 2. Generate variants
160
168
  result = engine.generate_wonder_variants(
@@ -194,8 +202,8 @@ def enter_wonder_mode(
194
202
  domain=thread_domain,
195
203
  )
196
204
  ws.creative_thread_id = thread.thread_id
197
- except Exception:
198
- pass
205
+ except Exception as exc:
206
+ logger.warning("open creative thread failed: %s", exc)
199
207
 
200
208
  # 5. Store session
201
209
  store_wonder_session(ws)
@@ -290,8 +298,8 @@ def discard_wonder_session(
290
298
  identity_effect="",
291
299
  user_sentiment="disliked",
292
300
  )
293
- except Exception:
294
- pass
301
+ except Exception as exc:
302
+ logger.warning("record_turn_resolution(rejected) failed: %s", exc)
295
303
 
296
304
  # Update taste graph — rejection is a negative signal for all executable variants
297
305
  try:
@@ -304,16 +312,16 @@ def discard_wonder_session(
304
312
  family=v["family"],
305
313
  kept=False,
306
314
  )
307
- except Exception:
308
- pass
315
+ except Exception as exc:
316
+ logger.warning("taste_graph negative-signal update failed: %s", exc)
309
317
 
310
318
  # Discard linked preview set
311
319
  if ws.preview_set_id:
312
320
  try:
313
321
  from ..preview_studio.engine import discard_set
314
322
  discard_set(ws.preview_set_id)
315
- except Exception:
316
- pass
323
+ except Exception as exc:
324
+ logger.warning("discard_set(%s) failed: %s", ws.preview_set_id, exc)
317
325
 
318
326
  return {
319
327
  "discarded": True,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.10.5",
3
+ "version": "1.10.7",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 320 tools, 43 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
5
+ "description": "Agentic production system for Ableton Live 12 — 323 tools, 45 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
6
6
  "author": "Pilot Studio",
7
7
  "license": "BSL-1.1",
8
8
  "type": "commonjs",
@@ -5,9 +5,10 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
5
5
  when this script is selected in Preferences > Link, Tempo & MIDI.
6
6
  """
7
7
 
8
- __version__ = "1.10.5"
8
+ __version__ = "1.10.7"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
+ from . import router
11
12
  from .server import LivePilotServer
12
13
  from . import transport # noqa: F401 — registers transport handlers
13
14
  from . import tracks # noqa: F401 — registers track handlers
@@ -23,8 +24,62 @@ from . import clip_automation # noqa: F401 — registers clip automation hand
23
24
  from . import version_detect # noqa: F401 — version detection
24
25
 
25
26
 
27
+ # ── Reload plumbing (BUG-B-reload, Batch 20) ──────────────────────────────
28
+ # Ableton keeps `sys.modules["LivePilot.*"]` cached across Control Surface
29
+ # toggles. Without intervention, edits to handler files don't take effect
30
+ # until a full Ableton restart — the toggle just re-instantiates the
31
+ # ControlSurface class without re-importing submodules.
32
+ #
33
+ # Fix: track whether this is the first create_instance call per
34
+ # interpreter lifetime. On subsequent calls, force-reload the router
35
+ # (which clears _handlers) and every handler module (which re-fires
36
+ # @register decorators with the updated code). Result: a Control Surface
37
+ # toggle now behaves like a fresh module reload, so live-editing mixing.py
38
+ # / devices.py / etc. and re-toggling is enough — no Ableton restart.
39
+
40
+ _FIRST_CREATE_INSTANCE = True
41
+
42
+ _HANDLER_MODULES = (
43
+ transport, tracks, clips, notes, devices, scenes,
44
+ mixing, browser, arrangement, diagnostics,
45
+ clip_automation, version_detect,
46
+ )
47
+
48
+
49
+ def _force_reload_handlers():
50
+ """Force Python to re-read the handler modules from disk.
51
+
52
+ Called on every create_instance() except the first, so edits to
53
+ handler files take effect via Control Surface toggle without
54
+ restarting Ableton. Order matters: router first (clears _handlers),
55
+ then each handler module (re-registers its @register decorators).
56
+ """
57
+ import importlib
58
+ try:
59
+ importlib.reload(router)
60
+ except Exception:
61
+ pass
62
+ for mod in _HANDLER_MODULES:
63
+ try:
64
+ importlib.reload(mod)
65
+ except Exception:
66
+ # Don't block Ableton startup on a single bad reload —
67
+ # the stale version will still work for that handler
68
+ pass
69
+
70
+
26
71
  def create_instance(c_instance):
27
- """Factory function called by Ableton Live."""
72
+ """Factory function called by Ableton Live.
73
+
74
+ Called once on initial Control Surface enable, AND every time the
75
+ user toggles the Control Surface off and on. The reload path below
76
+ makes the toggle behave like a fresh import — crucial for dev
77
+ ergonomics when iterating on mixing.py / devices.py / etc.
78
+ """
79
+ global _FIRST_CREATE_INSTANCE
80
+ if not _FIRST_CREATE_INSTANCE:
81
+ _force_reload_handlers()
82
+ _FIRST_CREATE_INSTANCE = False
28
83
  return LivePilot(c_instance)
29
84
 
30
85
 
@@ -36,6 +36,16 @@ def get_clip_info(song, params):
36
36
  if clip.is_audio_clip:
37
37
  result["warping"] = clip.warping
38
38
  result["warp_mode"] = clip.warp_mode
39
+ # BUG-A4: expose pitch/gain so callers can reason about sample
40
+ # transposition vs session key. Pitch is in semitones (int, -48..+48),
41
+ # pitch_fine in cents (-50..+50), gain linear (normalized 0..1 in Live).
42
+ # Some Live builds expose these as None on freshly-recorded clips
43
+ # before a warp pass; wrap in try/except for safety.
44
+ for attr in ("pitch_coarse", "pitch_fine", "gain"):
45
+ try:
46
+ result[attr] = getattr(clip, attr)
47
+ except AttributeError:
48
+ pass
39
49
 
40
50
  return result
41
51
 
@@ -202,6 +212,65 @@ def set_clip_launch(song, params):
202
212
  }
203
213
 
204
214
 
215
+ @register("set_clip_pitch")
216
+ def set_clip_pitch(song, params):
217
+ """Set pitch/gain on an audio clip (BUG-A5).
218
+
219
+ Audio clips only — MIDI clips raise ValueError.
220
+
221
+ Parameters
222
+ ----------
223
+ track_index, clip_index : int
224
+ coarse : int, optional -- semitones, -48..+48
225
+ fine : float, optional -- cents, -50.0..+50.0
226
+ gain : float, optional -- linear normalized (0..1 in Live)
227
+
228
+ At least one of (coarse, fine, gain) must be provided.
229
+ """
230
+ track_index = int(params["track_index"])
231
+ clip_index = int(params["clip_index"])
232
+ clip = get_clip(song, track_index, clip_index)
233
+
234
+ if clip.is_midi_clip:
235
+ raise ValueError("set_clip_pitch only works on audio clips")
236
+
237
+ coarse = params.get("coarse")
238
+ fine = params.get("fine")
239
+ gain = params.get("gain")
240
+
241
+ if coarse is None and fine is None and gain is None:
242
+ raise ValueError(
243
+ "Provide at least one of: coarse (semitones), fine (cents), gain (0-1)"
244
+ )
245
+
246
+ if coarse is not None:
247
+ c = int(coarse)
248
+ if c < -48 or c > 48:
249
+ raise ValueError("coarse must be in -48..+48 semitones")
250
+ clip.pitch_coarse = c
251
+ if fine is not None:
252
+ f = float(fine)
253
+ if f < -50.0 or f > 50.0:
254
+ raise ValueError("fine must be in -50..+50 cents")
255
+ clip.pitch_fine = f
256
+ if gain is not None:
257
+ g = float(gain)
258
+ if g < 0.0 or g > 1.0:
259
+ raise ValueError("gain must be in 0..1")
260
+ clip.gain = g
261
+
262
+ result = {
263
+ "track_index": track_index,
264
+ "clip_index": clip_index,
265
+ }
266
+ for attr in ("pitch_coarse", "pitch_fine", "gain"):
267
+ try:
268
+ result[attr] = getattr(clip, attr)
269
+ except AttributeError:
270
+ pass
271
+ return result
272
+
273
+
205
274
  @register("set_clip_warp_mode")
206
275
  def set_clip_warp_mode(song, params):
207
276
  """Set warp mode for an audio clip."""
@@ -293,3 +293,120 @@ def set_track_routing(song, params):
293
293
  result["output_routing_channel"] = track.output_routing_channel.display_name
294
294
 
295
295
  return result
296
+
297
+
298
+ @register("set_compressor_sidechain")
299
+ def set_compressor_sidechain(song, params):
300
+ """Configure a Compressor's sidechain input routing (BUG-A3).
301
+
302
+ Same LOM pattern as set_track_routing — the sidechain properties
303
+ (available_sidechain_input_routing_types / _channels) are on the
304
+ device object and Python's Remote Script accesses them cleanly.
305
+ This used to be routed through the M4L bridge (v1.10.6) but Max JS
306
+ LiveAPI couldn't read the available_* lists in Live 12.3.6, so it's
307
+ back on the Python side where it belongs.
308
+
309
+ Params:
310
+ track_index: 0+ regular, -1/-2 returns, -1000 master
311
+ device_index: Compressor's position in the chain
312
+ source_type (optional): RoutingType display name
313
+ (e.g. "1-DRUMS", "Ext. In", "No Input")
314
+ source_channel (optional): RoutingChannel display name
315
+ (e.g. "Post FX", "Pre FX", "Post Mixer")
316
+
317
+ Omit a param to leave that property unchanged.
318
+ """
319
+ track_index = int(params["track_index"])
320
+ device_index = int(params["device_index"])
321
+ source_type = params.get("source_type")
322
+ source_channel = params.get("source_channel")
323
+
324
+ track = get_track(song, track_index)
325
+ devices = list(track.devices)
326
+ if device_index < 0 or device_index >= len(devices):
327
+ raise ValueError(
328
+ "Device index %d out of range (track has %d devices)"
329
+ % (device_index, len(devices))
330
+ )
331
+ device = devices[device_index]
332
+
333
+ class_name = device.class_name
334
+ if class_name not in ("Compressor2", "Compressor"):
335
+ raise ValueError(
336
+ "Not a Compressor device (class is %s)" % class_name
337
+ )
338
+
339
+ # Older Compressor builds may not expose `sidechain_enabled` as a
340
+ # property; the automatable "S/C On" parameter is the fallback.
341
+ # Try both paths so the sidechain gets enabled whichever surface is
342
+ # available on this Live version.
343
+ try:
344
+ device.sidechain_enabled = True
345
+ except AttributeError:
346
+ # Fallback: find the "S/C On" parameter and toggle it
347
+ for param in device.parameters:
348
+ if param.name == "S/C On":
349
+ param.value = 1
350
+ break
351
+
352
+ result = {
353
+ "ok": True,
354
+ "track_index": track_index,
355
+ "device_index": device_index,
356
+ }
357
+
358
+ if source_type is not None and source_type != "":
359
+ if not hasattr(device, "available_sidechain_input_routing_types"):
360
+ raise ValueError(
361
+ "This Live build doesn't expose "
362
+ "device.available_sidechain_input_routing_types"
363
+ )
364
+ available = list(device.available_sidechain_input_routing_types)
365
+ matched = None
366
+ for rt in available:
367
+ if rt.display_name == source_type:
368
+ matched = rt
369
+ break
370
+ if matched is None:
371
+ options = [rt.display_name for rt in available]
372
+ raise ValueError(
373
+ "Sidechain input type '%s' not found. Available: %s"
374
+ % (source_type, ", ".join(options))
375
+ )
376
+ device.sidechain_input_routing_type = matched
377
+
378
+ if source_channel is not None and source_channel != "":
379
+ if not hasattr(device, "available_sidechain_input_routing_channels"):
380
+ raise ValueError(
381
+ "This Live build doesn't expose "
382
+ "device.available_sidechain_input_routing_channels"
383
+ )
384
+ available = list(device.available_sidechain_input_routing_channels)
385
+ matched = None
386
+ for ch in available:
387
+ if ch.display_name == source_channel:
388
+ matched = ch
389
+ break
390
+ if matched is None:
391
+ options = [ch.display_name for ch in available]
392
+ raise ValueError(
393
+ "Sidechain input channel '%s' not found. Available: %s"
394
+ % (source_channel, ", ".join(options))
395
+ )
396
+ device.sidechain_input_routing_channel = matched
397
+
398
+ # Read back canonical display names
399
+ try:
400
+ result["sidechain"] = {
401
+ "type": device.sidechain_input_routing_type.display_name,
402
+ "channel": device.sidechain_input_routing_channel.display_name,
403
+ "enabled": bool(getattr(device, "sidechain_enabled", True)),
404
+ }
405
+ except AttributeError:
406
+ # Very old Compressor — fall back to whatever we set above
407
+ result["sidechain"] = {
408
+ "type": source_type or "",
409
+ "channel": source_channel or "",
410
+ "enabled": True,
411
+ }
412
+ return result
@@ -62,8 +62,20 @@ def dispatch(song, command):
62
62
  )
63
63
 
64
64
  # Built-in ping — no handler registration needed.
65
+ # BUG-A1: embed remote_script version + supported command set so the
66
+ # MCP server can detect stale installs. An old Remote Script (pre-12.3
67
+ # handler adds) would return an older version here; the MCP server
68
+ # compares and warns the user to reinstall.
65
69
  if cmd_type == "ping":
66
- return success_response(request_id, {"pong": True})
70
+ try:
71
+ from . import __version__ as rs_version
72
+ except ImportError:
73
+ rs_version = "unknown"
74
+ return success_response(request_id, {
75
+ "pong": True,
76
+ "remote_script_version": rs_version,
77
+ "commands": sorted(_handlers.keys()),
78
+ })
67
79
 
68
80
  handler = _handlers.get(cmd_type)
69
81
  if handler is None:
@@ -51,45 +51,20 @@ def get_tools() -> list[dict]:
51
51
 
52
52
 
53
53
  def infer_domain(module: str) -> str:
54
- """Infer domain from module path."""
55
- if "semantic_moves" in module:
56
- return "Semantic Moves"
57
- if "experiment" in module:
58
- return "Experiments"
59
- if "musical_intelligence" in module:
60
- return "Musical Intelligence"
61
- if "memory.tools" in module:
62
- return "Memory Fabric"
63
- if "mix_engine" in module:
64
- return "Mix Engine"
65
- if "sound_design" in module:
66
- return "Sound Design"
67
- if "transition_engine" in module:
68
- return "Transition Engine"
69
- if "reference_engine" in module:
70
- return "Reference Engine"
71
- if "translation_engine" in module:
72
- return "Translation Engine"
73
- if "performance_engine" in module:
74
- return "Performance Engine"
75
- if "project_brain" in module:
76
- return "Project Brain"
77
- if "evaluation" in module:
78
- return "Evaluation"
79
- if "runtime" in module:
80
- return "Runtime"
81
-
82
- # Core tools — extract from module name
54
+ """Infer domain from module path using the same module-layout rule as
55
+ ``scripts/sync_metadata.py``:
56
+ - ``mcp_server.<X>.<...>`` → ``<X>``
57
+ - ``mcp_server.tools.<Y>`` → ``<Y>``
58
+ The display name is Title-Cased; underscores become spaces.
59
+ """
83
60
  parts = module.split(".")
84
- for p in reversed(parts):
85
- if p in ("transport", "tracks", "clips", "notes", "devices", "scenes",
86
- "mixing", "browser", "arrangement", "memory", "analyzer",
87
- "automation", "theory", "generative", "harmony", "midi_io",
88
- "perception", "agent_os", "composition", "motif", "research",
89
- "planner"):
90
- return p.replace("_", " ").title()
91
-
92
- return "Other"
61
+ if len(parts) < 2 or parts[0] != "mcp_server":
62
+ return "Other"
63
+ if parts[1] == "tools":
64
+ domain_key = parts[2] if len(parts) > 2 else "other"
65
+ else:
66
+ domain_key = parts[1]
67
+ return domain_key.replace("_", " ").title()
93
68
 
94
69
 
95
70
  def main():