livepilot 1.9.23 → 1.10.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 (191) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +119 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +144 -13
  6. package/bin/livepilot.js +87 -0
  7. package/installer/codex.js +147 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/skills/livepilot-core/SKILL.md +21 -4
  11. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
  12. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
  13. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
  14. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
  15. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
  16. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
  17. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
  18. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
  19. package/livepilot/skills/livepilot-core/references/overview.md +13 -9
  20. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
  21. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
  22. package/livepilot/skills/livepilot-devices/SKILL.md +16 -2
  23. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  24. package/livepilot/skills/livepilot-release/SKILL.md +19 -5
  25. package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -0
  26. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
  27. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
  28. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
  29. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
  30. package/livepilot/skills/livepilot-wonder/SKILL.md +15 -0
  31. package/livepilot.mcpb +0 -0
  32. package/m4l_device/livepilot_bridge.js +1 -1
  33. package/manifest.json +2 -2
  34. package/mcp_server/__init__.py +1 -1
  35. package/mcp_server/atlas/__init__.py +357 -0
  36. package/mcp_server/atlas/device_atlas.json +44067 -0
  37. package/mcp_server/atlas/enrichments/__init__.py +111 -0
  38. package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
  39. package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
  40. package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
  41. package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
  42. package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
  43. package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
  44. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
  45. package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
  46. package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
  47. package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
  48. package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
  49. package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
  50. package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
  51. package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
  52. package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
  53. package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
  54. package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
  55. package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
  56. package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
  57. package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
  58. package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
  59. package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
  60. package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
  61. package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
  62. package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
  63. package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
  64. package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
  65. package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
  66. package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
  67. package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
  68. package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
  69. package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
  70. package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
  71. package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
  72. package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
  73. package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
  74. package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
  75. package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
  76. package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
  77. package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
  78. package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
  79. package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
  80. package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
  81. package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
  82. package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
  83. package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
  84. package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
  85. package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
  86. package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
  87. package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
  88. package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
  89. package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
  90. package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
  91. package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
  92. package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
  93. package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
  94. package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
  95. package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
  96. package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
  97. package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
  98. package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
  99. package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
  100. package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
  101. package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
  102. package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
  103. package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
  104. package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
  105. package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
  106. package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
  107. package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
  108. package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
  109. package/mcp_server/atlas/scanner.py +236 -0
  110. package/mcp_server/atlas/tools.py +224 -0
  111. package/mcp_server/composer/__init__.py +1 -0
  112. package/mcp_server/composer/engine.py +452 -0
  113. package/mcp_server/composer/layer_planner.py +427 -0
  114. package/mcp_server/composer/prompt_parser.py +329 -0
  115. package/mcp_server/composer/tools.py +201 -0
  116. package/mcp_server/connection.py +53 -8
  117. package/mcp_server/corpus/__init__.py +377 -0
  118. package/mcp_server/device_forge/__init__.py +1 -0
  119. package/mcp_server/device_forge/builder.py +377 -0
  120. package/mcp_server/device_forge/models.py +142 -0
  121. package/mcp_server/device_forge/templates.py +483 -0
  122. package/mcp_server/device_forge/tools.py +162 -0
  123. package/mcp_server/hook_hunter/analyzer.py +23 -0
  124. package/mcp_server/hook_hunter/models.py +1 -0
  125. package/mcp_server/hook_hunter/tools.py +4 -2
  126. package/mcp_server/m4l_bridge.py +1 -0
  127. package/mcp_server/memory/taste_graph.py +68 -1
  128. package/mcp_server/memory/tools.py +15 -4
  129. package/mcp_server/musical_intelligence/detectors.py +14 -1
  130. package/mcp_server/musical_intelligence/tools.py +11 -8
  131. package/mcp_server/persistence/__init__.py +1 -0
  132. package/mcp_server/persistence/base_store.py +82 -0
  133. package/mcp_server/persistence/project_store.py +106 -0
  134. package/mcp_server/persistence/taste_store.py +122 -0
  135. package/mcp_server/preview_studio/models.py +1 -0
  136. package/mcp_server/preview_studio/tools.py +56 -13
  137. package/mcp_server/runtime/capability.py +66 -0
  138. package/mcp_server/runtime/capability_probe.py +137 -0
  139. package/mcp_server/runtime/execution_router.py +143 -0
  140. package/mcp_server/runtime/live_version.py +102 -0
  141. package/mcp_server/runtime/remote_commands.py +87 -0
  142. package/mcp_server/runtime/tools.py +18 -4
  143. package/mcp_server/sample_engine/__init__.py +1 -0
  144. package/mcp_server/sample_engine/analyzer.py +216 -0
  145. package/mcp_server/sample_engine/critics.py +390 -0
  146. package/mcp_server/sample_engine/models.py +193 -0
  147. package/mcp_server/sample_engine/moves.py +127 -0
  148. package/mcp_server/sample_engine/planner.py +186 -0
  149. package/mcp_server/sample_engine/sources.py +540 -0
  150. package/mcp_server/sample_engine/techniques.py +908 -0
  151. package/mcp_server/sample_engine/tools.py +442 -0
  152. package/mcp_server/semantic_moves/__init__.py +3 -0
  153. package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
  154. package/mcp_server/semantic_moves/mix_moves.py +41 -41
  155. package/mcp_server/semantic_moves/performance_moves.py +13 -13
  156. package/mcp_server/semantic_moves/sample_compilers.py +372 -0
  157. package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
  158. package/mcp_server/semantic_moves/tools.py +18 -17
  159. package/mcp_server/semantic_moves/transition_moves.py +16 -16
  160. package/mcp_server/server.py +51 -0
  161. package/mcp_server/services/__init__.py +1 -0
  162. package/mcp_server/services/motif_service.py +67 -0
  163. package/mcp_server/session_continuity/tracker.py +29 -1
  164. package/mcp_server/song_brain/builder.py +28 -1
  165. package/mcp_server/song_brain/models.py +4 -0
  166. package/mcp_server/song_brain/tools.py +20 -2
  167. package/mcp_server/sound_design/critics.py +89 -1
  168. package/mcp_server/splice_client/__init__.py +1 -0
  169. package/mcp_server/splice_client/client.py +347 -0
  170. package/mcp_server/splice_client/models.py +96 -0
  171. package/mcp_server/splice_client/protos/__init__.py +1 -0
  172. package/mcp_server/splice_client/protos/app_pb2.py +319 -0
  173. package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
  174. package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
  175. package/mcp_server/tools/arrangement.py +69 -0
  176. package/mcp_server/tools/automation.py +15 -2
  177. package/mcp_server/tools/devices.py +117 -6
  178. package/mcp_server/tools/notes.py +37 -4
  179. package/mcp_server/wonder_mode/diagnosis.py +5 -0
  180. package/mcp_server/wonder_mode/engine.py +85 -1
  181. package/mcp_server/wonder_mode/tools.py +6 -1
  182. package/package.json +12 -2
  183. package/remote_script/LivePilot/__init__.py +8 -1
  184. package/remote_script/LivePilot/arrangement.py +114 -0
  185. package/remote_script/LivePilot/browser.py +56 -1
  186. package/remote_script/LivePilot/devices.py +236 -6
  187. package/remote_script/LivePilot/mixing.py +8 -3
  188. package/remote_script/LivePilot/server.py +5 -1
  189. package/remote_script/LivePilot/transport.py +3 -0
  190. package/remote_script/LivePilot/version_detect.py +78 -0
  191. package/scripts/sync_metadata.py +132 -0
@@ -0,0 +1,236 @@
1
+ """
2
+ Device Atlas scanner — transforms raw browser scan data into atlas entries.
3
+
4
+ Converts the flat {categories: {cat: [items]}} payload from scan_browser_deep
5
+ into normalised device dicts ready for enrichment and querying.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any
12
+
13
+
14
+ # ── ID generation ────────────────────────────────────────────────────────────
15
+
16
+ def make_device_id(name: str, prefix: str = "") -> str:
17
+ """Convert a human-readable device name to a snake_case identifier.
18
+
19
+ >>> make_device_id("EQ Eight")
20
+ 'eq_eight'
21
+ >>> make_device_id("Model D", prefix="auv3_moog")
22
+ 'auv3_moog_model_d'
23
+ """
24
+ slug = re.sub(r"[^a-zA-Z0-9]+", "_", name).strip("_").lower()
25
+ if prefix:
26
+ prefix_slug = re.sub(r"[^a-zA-Z0-9]+", "_", prefix).strip("_").lower()
27
+ return f"{prefix_slug}_{slug}"
28
+ return slug
29
+
30
+
31
+ # ── Category / subcategory mapping ───────────────────────────────────────────
32
+
33
+ _CATEGORY_MAP: dict[str, str] = {
34
+ "instruments": "instruments",
35
+ "audio_effects": "audio_effects",
36
+ "midi_effects": "midi_effects",
37
+ "drums": "drum_kits",
38
+ "max_for_live": "max_for_live",
39
+ "plugins": "plugins",
40
+ "sounds": "sounds",
41
+ }
42
+
43
+ _INSTRUMENT_SUBCATEGORIES: dict[str, str] = {
44
+ # Synths
45
+ "analog": "synths",
46
+ "wavetable": "synths",
47
+ "operator": "synths",
48
+ "drift": "synths",
49
+ "meld": "synths",
50
+ "emit": "synths",
51
+ "poli": "synths",
52
+ "tree_tone": "synths",
53
+ "vector_fm": "synths",
54
+ "vector_grain": "synths",
55
+ "bass": "synths",
56
+ # Physical modelling
57
+ "collision": "physical_modeling",
58
+ "tension": "physical_modeling",
59
+ "electric": "physical_modeling",
60
+ # Samplers
61
+ "simpler": "samplers",
62
+ "sampler": "samplers",
63
+ # Drums
64
+ "drum_rack": "drums",
65
+ "drum_sampler": "drums",
66
+ "impulse": "drums",
67
+ # Racks
68
+ "instrument_rack": "racks",
69
+ # Routing
70
+ "external_instrument": "routing",
71
+ # Granular
72
+ "granulator_iii": "granular",
73
+ }
74
+
75
+ _AUDIO_EFFECT_SUBCATEGORIES: dict[str, str] = {
76
+ # Dynamics
77
+ "compressor": "dynamics",
78
+ "glue_compressor": "dynamics",
79
+ "limiter": "dynamics",
80
+ "color_limiter": "dynamics",
81
+ "multiband_dynamics": "dynamics",
82
+ "gate": "dynamics",
83
+ "drum_buss": "dynamics",
84
+ "re_enveloper": "dynamics",
85
+ # EQ
86
+ "eq_eight": "eq",
87
+ "eq_three": "eq",
88
+ "channel_eq": "eq",
89
+ # Filter
90
+ "auto_filter": "filter",
91
+ "spectral_resonator": "filter",
92
+ # Delay
93
+ "delay": "delay",
94
+ "echo": "delay",
95
+ "grain_delay": "delay",
96
+ "filter_delay": "delay",
97
+ "gated_delay": "delay",
98
+ "vector_delay": "delay",
99
+ "beat_repeat": "delay",
100
+ "spectral_time": "delay",
101
+ "align_delay": "delay",
102
+ # Reverb
103
+ "reverb": "reverb",
104
+ "hybrid_reverb": "reverb",
105
+ "convolution_reverb": "reverb",
106
+ "convolution_reverb_pro": "reverb",
107
+ # Distortion
108
+ "saturator": "distortion",
109
+ "overdrive": "distortion",
110
+ "pedal": "distortion",
111
+ "roar": "distortion",
112
+ "dynamic_tube": "distortion",
113
+ "erosion": "distortion",
114
+ "redux": "distortion",
115
+ "vinyl_distortion": "distortion",
116
+ "amp": "distortion",
117
+ "cabinet": "distortion",
118
+ # Modulation
119
+ "chorus_ensemble": "modulation",
120
+ "phaser_flanger": "modulation",
121
+ "shifter": "modulation",
122
+ "auto_pan_tremolo": "modulation",
123
+ "auto_shift": "modulation",
124
+ "shaper": "modulation",
125
+ "lfo": "modulation",
126
+ "envelope_follower": "modulation",
127
+ "vector_map": "modulation",
128
+ # Utility
129
+ "utility": "utility",
130
+ "spectrum": "utility",
131
+ "tuner": "utility",
132
+ "variations": "utility",
133
+ "prearranger": "utility",
134
+ # Spatial
135
+ "surround_panner": "spatial",
136
+ # Performance
137
+ "looper": "performance",
138
+ "arrangement_looper": "performance",
139
+ "performer": "performance",
140
+ # Spectral
141
+ "spectral_blur": "spectral",
142
+ # Pitch
143
+ "pitch_hack": "pitch",
144
+ "pitchloop89": "pitch",
145
+ # Physical modelling
146
+ "corpus": "physical_modeling",
147
+ "resonators": "physical_modeling",
148
+ # Special
149
+ "vocoder": "special",
150
+ # Racks
151
+ "audio_effect_rack": "racks",
152
+ # Routing
153
+ "external_audio_effect": "routing",
154
+ }
155
+
156
+
157
+ def _classify_subcategory(device_id: str, category: str) -> str:
158
+ """Return the subcategory for a device based on its id and category."""
159
+ if category == "instruments":
160
+ return _INSTRUMENT_SUBCATEGORIES.get(device_id, "other")
161
+ if category == "audio_effects":
162
+ return _AUDIO_EFFECT_SUBCATEGORIES.get(device_id, "other")
163
+ return "other"
164
+
165
+
166
+ # ── Empty device template ────────────────────────────────────────────────────
167
+
168
+ def _empty_device(
169
+ device_id: str,
170
+ name: str,
171
+ uri: str | None,
172
+ category: str,
173
+ subcategory: str,
174
+ source: str,
175
+ ) -> dict[str, Any]:
176
+ """Return a skeleton device dict with all atlas fields set to defaults."""
177
+ return {
178
+ "id": device_id,
179
+ "name": name,
180
+ "uri": uri,
181
+ "category": category,
182
+ "subcategory": subcategory,
183
+ "source": source,
184
+ "enriched": False,
185
+ "character_tags": [],
186
+ "use_cases": [],
187
+ "genre_affinity": {"primary": [], "secondary": []},
188
+ "self_contained": True,
189
+ "key_parameters": [],
190
+ "pairs_well_with": [],
191
+ "starter_recipes": [],
192
+ "gotchas": [],
193
+ "health_flags": [],
194
+ }
195
+
196
+
197
+ # ── Normaliser ───────────────────────────────────────────────────────────────
198
+
199
+ def normalize_scan_results(raw_scan: dict[str, Any]) -> list[dict[str, Any]]:
200
+ """Convert raw scan_browser_deep output to a flat list of device dicts.
201
+
202
+ Parameters
203
+ ----------
204
+ raw_scan : dict
205
+ ``{"categories": {cat_name: [{"name", "uri", "is_loadable"}, ...], ...}}``
206
+
207
+ Returns
208
+ -------
209
+ list[dict]
210
+ Deduplicated device entries with all atlas fields initialised.
211
+ """
212
+ categories_data = raw_scan.get("categories", {})
213
+ seen_uris: set[str] = set()
214
+ devices: list[dict[str, Any]] = []
215
+
216
+ for raw_cat, items in categories_data.items():
217
+ category = _CATEGORY_MAP.get(raw_cat, raw_cat)
218
+ source = "native" if raw_cat in _CATEGORY_MAP else raw_cat
219
+
220
+ for item in items:
221
+ name = item.get("name", "")
222
+ uri = item.get("uri")
223
+
224
+ # Deduplicate by URI when available
225
+ if uri and uri in seen_uris:
226
+ continue
227
+ if uri:
228
+ seen_uris.add(uri)
229
+
230
+ device_id = make_device_id(name)
231
+ subcategory = _classify_subcategory(device_id, category)
232
+ devices.append(
233
+ _empty_device(device_id, name, uri, category, subcategory, source)
234
+ )
235
+
236
+ return devices
@@ -0,0 +1,224 @@
1
+ """Atlas MCP tools — search, suggest, compare, and scan the device database.
2
+
3
+ 6 tools for the atlas domain.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import time
11
+
12
+ from fastmcp import Context
13
+
14
+ from ..server import mcp
15
+
16
+
17
+ def _get_ableton(ctx: Context):
18
+ return ctx.lifespan_context["ableton"]
19
+
20
+
21
+ def _get_atlas():
22
+ """Get the global AtlasManager instance, loading lazily if needed."""
23
+ from . import _atlas_instance, _load_atlas
24
+ if _atlas_instance is None:
25
+ try:
26
+ _load_atlas()
27
+ except FileNotFoundError:
28
+ return None
29
+ from . import _atlas_instance as inst
30
+ return inst
31
+
32
+
33
+ @mcp.tool()
34
+ def atlas_search(ctx: Context, query: str, category: str = "all", limit: int = 10) -> dict:
35
+ """Search the device atlas for instruments, effects, kits, or plugins.
36
+
37
+ query: natural language search — name, sonic character, use case, or genre
38
+ Examples: "warm analog bass", "reverb", "808 kit", "granular"
39
+ category: filter by category (all, instruments, audio_effects, midi_effects,
40
+ max_for_live, drum_kits, plugins)
41
+ limit: max results (default 10)
42
+ """
43
+ atlas = _get_atlas()
44
+ if atlas is None:
45
+ return {"error": "Atlas not loaded. Run scan_full_library first.", "results": []}
46
+
47
+ results = atlas.search(query, category=category, limit=limit)
48
+ return {
49
+ "query": query,
50
+ "category": category,
51
+ "count": len(results),
52
+ "results": [
53
+ {
54
+ "id": r["device"].get("id", ""),
55
+ "name": r["device"].get("name", ""),
56
+ "uri": r["device"].get("uri", ""),
57
+ "category": r["device"].get("category", ""),
58
+ "sonic_description": r["device"].get("sonic_description", "")[:120],
59
+ "character_tags": r["device"].get("character_tags", [])[:5],
60
+ "enriched": r["device"].get("enriched", False),
61
+ "score": r.get("score", 0),
62
+ }
63
+ for r in results
64
+ ],
65
+ }
66
+
67
+
68
+ @mcp.tool()
69
+ def atlas_device_info(ctx: Context, device_id: str) -> dict:
70
+ """Get complete atlas knowledge about a device — parameters, recipes, pairings, gotchas.
71
+
72
+ device_id: the atlas ID or device name (e.g., "drift", "Compressor", "808_core_kit")
73
+ """
74
+ atlas = _get_atlas()
75
+ if atlas is None:
76
+ return {"error": "Atlas not loaded. Run scan_full_library first."}
77
+
78
+ entry = atlas.lookup(device_id)
79
+ if entry is None:
80
+ return {"error": f"Device '{device_id}' not found in atlas", "suggestion": "Use atlas_search to find devices"}
81
+ return entry
82
+
83
+
84
+ @mcp.tool()
85
+ def atlas_suggest(
86
+ ctx: Context,
87
+ intent: str,
88
+ genre: str = "",
89
+ energy: str = "medium",
90
+ key: str = "",
91
+ ) -> dict:
92
+ """Suggest devices for a production intent.
93
+
94
+ intent: what you're trying to achieve — "warm bass", "crispy hi-hats", "evolving texture"
95
+ genre: target genre for better recommendations
96
+ energy: low/medium/high — affects sonic character suggestions
97
+ key: musical key context (e.g., "Cm") for tuned percussion suggestions
98
+ """
99
+ atlas = _get_atlas()
100
+ if atlas is None:
101
+ return {"error": "Atlas not loaded. Run scan_full_library first."}
102
+
103
+ results = atlas.suggest(intent, genre=genre, energy=energy)
104
+ return {
105
+ "intent": intent,
106
+ "genre": genre,
107
+ "energy": energy,
108
+ "suggestions": [
109
+ {
110
+ "device_id": r["device"]["id"],
111
+ "device_name": r["device"]["name"],
112
+ "uri": r["device"].get("uri", ""),
113
+ "rationale": r["rationale"],
114
+ "recipe": r.get("recipe"),
115
+ }
116
+ for r in results
117
+ ],
118
+ }
119
+
120
+
121
+ @mcp.tool()
122
+ def atlas_chain_suggest(ctx: Context, role: str, genre: str = "") -> dict:
123
+ """Suggest a full device chain for a track role.
124
+
125
+ role: the musical role — "bass", "lead", "pad", "drums", "percussion", "texture"
126
+ genre: target genre for style-appropriate choices
127
+ """
128
+ atlas = _get_atlas()
129
+ if atlas is None:
130
+ return {"error": "Atlas not loaded. Run scan_full_library first."}
131
+
132
+ return atlas.chain_suggest(role, genre=genre)
133
+
134
+
135
+ @mcp.tool()
136
+ def atlas_compare(ctx: Context, device_a: str, device_b: str, role: str = "") -> dict:
137
+ """Compare two devices — strengths, weaknesses, and recommendation for a role.
138
+
139
+ device_a: first device name or ID
140
+ device_b: second device name or ID
141
+ role: optional role context (e.g., "bass", "pad")
142
+ """
143
+ atlas = _get_atlas()
144
+ if atlas is None:
145
+ return {"error": "Atlas not loaded. Run scan_full_library first."}
146
+
147
+ return atlas.compare(device_a, device_b, role=role)
148
+
149
+
150
+ @mcp.tool()
151
+ def scan_full_library(ctx: Context, force: bool = False) -> dict:
152
+ """Scan the full Ableton browser and rebuild the device atlas.
153
+
154
+ Walks every category (instruments, audio_effects, midi_effects, max_for_live,
155
+ drums, plugins, packs) and records every loadable item with its URI.
156
+ Results are merged with curated enrichments and saved to device_atlas.json.
157
+
158
+ force: if True, rescan even if atlas already exists (default False)
159
+ """
160
+ from .scanner import normalize_scan_results
161
+ from .enrichments import load_enrichments, merge_enrichments
162
+ from . import AtlasManager
163
+
164
+ atlas_dir = os.path.dirname(os.path.abspath(__file__))
165
+ atlas_path = os.path.join(atlas_dir, "device_atlas.json")
166
+ enrichments_dir = os.path.join(atlas_dir, "enrichments")
167
+
168
+ if not force and os.path.exists(atlas_path):
169
+ age = time.time() - os.path.getmtime(atlas_path)
170
+ if age < 86400:
171
+ # Reload if not already loaded
172
+ import mcp_server.atlas as atlas_mod
173
+ if atlas_mod._atlas_instance is None:
174
+ atlas_mod._atlas_instance = AtlasManager(atlas_path)
175
+ return {
176
+ "status": "already_exists",
177
+ "age_hours": round(age / 3600, 1),
178
+ "device_count": atlas_mod._atlas_instance.device_count,
179
+ "message": "Atlas is recent. Use force=True to rescan.",
180
+ }
181
+
182
+ # Scan browser
183
+ ableton = _get_ableton(ctx)
184
+ raw = ableton.send_command("scan_browser_deep", {"max_per_category": 1000})
185
+
186
+ # Normalize
187
+ devices = normalize_scan_results(raw)
188
+
189
+ # Load and merge enrichments
190
+ enrichments = load_enrichments(enrichments_dir)
191
+ devices = merge_enrichments(devices, enrichments)
192
+
193
+ # Count stats
194
+ stats: dict = {"total_devices": len(devices)}
195
+ for device in devices:
196
+ cat = device.get("category", "other")
197
+ stats[cat] = stats.get(cat, 0) + 1
198
+ stats["enriched_devices"] = sum(1 for d in devices if d.get("enriched"))
199
+
200
+ # Build atlas
201
+ atlas_data = {
202
+ "version": "2.0.0",
203
+ "live_version": "12.3.6",
204
+ "scanned_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
205
+ "stats": stats,
206
+ "devices": devices,
207
+ "packs": [],
208
+ }
209
+
210
+ # Write
211
+ with open(atlas_path, "w") as f:
212
+ json.dump(atlas_data, f, indent=2)
213
+
214
+ # Reload into global
215
+ import mcp_server.atlas as atlas_mod
216
+ atlas_mod._atlas_instance = AtlasManager(atlas_path)
217
+
218
+ return {
219
+ "status": "scanned",
220
+ "device_count": len(devices),
221
+ "enriched_count": stats["enriched_devices"],
222
+ "stats": stats,
223
+ "atlas_path": atlas_path,
224
+ }
@@ -0,0 +1 @@
1
+ """Composer Engine — auto-composition from text prompts via Splice + Sample Engine."""