livepilot 1.16.1 → 1.17.1
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.
- package/CHANGELOG.md +311 -0
- package/README.md +16 -15
- package/installer/codex.js +14 -0
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +85 -0
- package/mcp_server/atlas/device_atlas.json +3183 -382
- package/mcp_server/atlas/device_techniques_index.json +1510 -0
- package/mcp_server/atlas/enrichments/__init__.py +1 -0
- package/mcp_server/atlas/enrichments/audio_effects/amp.yaml +112 -0
- package/mcp_server/atlas/enrichments/audio_effects/audio_effect_rack.yaml +77 -0
- package/mcp_server/atlas/enrichments/audio_effects/cabinet.yaml +81 -0
- package/mcp_server/atlas/enrichments/audio_effects/corpus.yaml +128 -0
- package/mcp_server/atlas/enrichments/audio_effects/envelope_follower.yaml +99 -0
- package/mcp_server/atlas/enrichments/audio_effects/external_audio_effect.yaml +64 -0
- package/mcp_server/atlas/enrichments/audio_effects/looper.yaml +85 -0
- package/mcp_server/atlas/enrichments/audio_effects/resonators.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/snipper.yaml +17 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectrum.yaml +61 -0
- package/mcp_server/atlas/enrichments/audio_effects/tuner.yaml +43 -0
- package/mcp_server/atlas/enrichments/audio_effects/utility.yaml +118 -0
- package/mcp_server/atlas/enrichments/audio_effects/vocoder.yaml +94 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/bell_tower.yaml +38 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/drum_rack.yaml +142 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +12 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +15 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +11 -0
- package/mcp_server/atlas/enrichments/midi_effects/filler.yaml +17 -0
- package/mcp_server/atlas/enrichments/utility/performer.yaml +15 -0
- package/mcp_server/atlas/enrichments/utility/vector_map.yaml +21 -0
- package/mcp_server/atlas/tools.py +291 -0
- package/mcp_server/m4l_bridge.py +19 -2
- package/mcp_server/sample_engine/tools.py +201 -128
- package/mcp_server/splice_client/http_bridge.py +319 -116
- package/mcp_server/tools/automation.py +168 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +216 -1
- package/server.json +3 -3
|
@@ -259,14 +259,10 @@ async def search_samples(
|
|
|
259
259
|
# Splice search — prefer gRPC online catalog when available, fall back
|
|
260
260
|
# to local SQLite index. See docs/2026-04-14-bugs-discovered.md — P0-2.
|
|
261
261
|
if source in (None, "splice"):
|
|
262
|
-
grpc_client =
|
|
263
|
-
try:
|
|
264
|
-
grpc_client = ctx.lifespan_context.get("splice_client")
|
|
265
|
-
except AttributeError:
|
|
266
|
-
grpc_client = None
|
|
262
|
+
grpc_client = await _ensure_splice_client_connected(ctx)
|
|
267
263
|
|
|
268
264
|
used_grpc = False
|
|
269
|
-
if grpc_client is not None
|
|
265
|
+
if grpc_client is not None:
|
|
270
266
|
try:
|
|
271
267
|
grpc_result = await grpc_client.search_samples(
|
|
272
268
|
query=query,
|
|
@@ -712,6 +708,43 @@ _SPLICE_USER_LIB_DEST = "~/Music/Ableton/User Library/Samples/Splice"
|
|
|
712
708
|
_SPLICE_PREVIEW_CACHE = "~/Library/Caches/LivePilot/splice_previews"
|
|
713
709
|
|
|
714
710
|
|
|
711
|
+
def _get_splice_client_from_context(ctx: Context):
|
|
712
|
+
"""Return the shared Splice client from lifespan context when present."""
|
|
713
|
+
try:
|
|
714
|
+
return ctx.lifespan_context.get("splice_client")
|
|
715
|
+
except AttributeError:
|
|
716
|
+
return None
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
async def _ensure_splice_client_connected(ctx: Context):
|
|
720
|
+
"""Reconnect the shared Splice client on demand.
|
|
721
|
+
|
|
722
|
+
The MCP server creates one long-lived client during startup. If that
|
|
723
|
+
first handshake races or Splice launches later, the old behavior kept
|
|
724
|
+
every tool stuck in a disconnected state until the whole MCP server
|
|
725
|
+
restarted. Re-check here so tool results reflect current desktop state.
|
|
726
|
+
"""
|
|
727
|
+
client = _get_splice_client_from_context(ctx)
|
|
728
|
+
if client is None:
|
|
729
|
+
return None
|
|
730
|
+
if getattr(client, "connected", False):
|
|
731
|
+
return client
|
|
732
|
+
|
|
733
|
+
connect = getattr(client, "connect", None)
|
|
734
|
+
if connect is None:
|
|
735
|
+
return None
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
await connect()
|
|
739
|
+
except Exception as exc:
|
|
740
|
+
logger.debug("Splice reconnect failed: %s", exc)
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
if getattr(client, "connected", False):
|
|
744
|
+
return client
|
|
745
|
+
return None
|
|
746
|
+
|
|
747
|
+
|
|
715
748
|
@mcp.tool()
|
|
716
749
|
async def get_splice_credits(ctx: Context) -> dict:
|
|
717
750
|
"""Get the user's current Splice plan, credits, and daily sample quota.
|
|
@@ -745,15 +778,10 @@ async def get_splice_credits(ctx: Context) -> dict:
|
|
|
745
778
|
from ..splice_client.models import PlanKind
|
|
746
779
|
from ..splice_client.quota import get_tracker
|
|
747
780
|
|
|
748
|
-
client = None
|
|
749
|
-
try:
|
|
750
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
751
|
-
except AttributeError:
|
|
752
|
-
pass
|
|
753
|
-
|
|
754
781
|
quota_summary = get_tracker().summary()
|
|
782
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
755
783
|
|
|
756
|
-
if client is None
|
|
784
|
+
if client is None:
|
|
757
785
|
return {
|
|
758
786
|
"connected": False,
|
|
759
787
|
"username": "",
|
|
@@ -857,13 +885,8 @@ async def splice_catalog_hunt(
|
|
|
857
885
|
Each sample entry contains `file_hash` which you can pass to
|
|
858
886
|
`splice_download_sample` to trigger a download.
|
|
859
887
|
"""
|
|
860
|
-
client =
|
|
861
|
-
|
|
862
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
863
|
-
except AttributeError:
|
|
864
|
-
pass
|
|
865
|
-
|
|
866
|
-
if client is None or not getattr(client, "connected", False):
|
|
888
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
889
|
+
if client is None:
|
|
867
890
|
return {
|
|
868
891
|
"connected": False,
|
|
869
892
|
"error": "Splice gRPC not connected",
|
|
@@ -970,13 +993,8 @@ async def splice_download_sample(
|
|
|
970
993
|
"""
|
|
971
994
|
import shutil
|
|
972
995
|
|
|
973
|
-
client =
|
|
974
|
-
|
|
975
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
976
|
-
except AttributeError:
|
|
977
|
-
pass
|
|
978
|
-
|
|
979
|
-
if client is None or not getattr(client, "connected", False):
|
|
996
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
997
|
+
if client is None:
|
|
980
998
|
return {
|
|
981
999
|
"ok": False,
|
|
982
1000
|
"error": "Splice gRPC not connected",
|
|
@@ -1096,13 +1114,8 @@ async def splice_preview_sample(
|
|
|
1096
1114
|
import urllib.request
|
|
1097
1115
|
import urllib.error
|
|
1098
1116
|
|
|
1099
|
-
client =
|
|
1100
|
-
|
|
1101
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
1102
|
-
except AttributeError:
|
|
1103
|
-
pass
|
|
1104
|
-
|
|
1105
|
-
if client is None or not getattr(client, "connected", False):
|
|
1117
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
1118
|
+
if client is None:
|
|
1106
1119
|
return {"ok": False, "error": "Splice gRPC not connected"}
|
|
1107
1120
|
|
|
1108
1121
|
# Two-stage lookup: SampleInfo is the fast path but only returns
|
|
@@ -1184,14 +1197,10 @@ async def splice_preview_sample(
|
|
|
1184
1197
|
# ────────────────────────────────────────────────────────────────────────
|
|
1185
1198
|
|
|
1186
1199
|
|
|
1187
|
-
def _require_splice_client(ctx: Context) -> tuple[object, Optional[dict]]:
|
|
1200
|
+
async def _require_splice_client(ctx: Context) -> tuple[object, Optional[dict]]:
|
|
1188
1201
|
"""Fetch the Splice client from context, or return an error dict."""
|
|
1189
|
-
client =
|
|
1190
|
-
|
|
1191
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
1192
|
-
except AttributeError:
|
|
1193
|
-
pass
|
|
1194
|
-
if client is None or not getattr(client, "connected", False):
|
|
1202
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
1203
|
+
if client is None:
|
|
1195
1204
|
return None, {"ok": False, "error": "Splice gRPC not connected"}
|
|
1196
1205
|
return client, None
|
|
1197
1206
|
|
|
@@ -1215,7 +1224,7 @@ async def splice_list_collections(
|
|
|
1215
1224
|
],
|
|
1216
1225
|
}
|
|
1217
1226
|
"""
|
|
1218
|
-
client, err = _require_splice_client(ctx)
|
|
1227
|
+
client, err = await _require_splice_client(ctx)
|
|
1219
1228
|
if err:
|
|
1220
1229
|
return err
|
|
1221
1230
|
total, collections = await client.list_collections(
|
|
@@ -1244,7 +1253,7 @@ async def splice_search_in_collection(
|
|
|
1244
1253
|
`splice_catalog_hunt` — you can feed them straight into
|
|
1245
1254
|
`splice_preview_sample` or `splice_download_sample`.
|
|
1246
1255
|
"""
|
|
1247
|
-
client, err = _require_splice_client(ctx)
|
|
1256
|
+
client, err = await _require_splice_client(ctx)
|
|
1248
1257
|
if err:
|
|
1249
1258
|
return err
|
|
1250
1259
|
total, samples = await client.collection_samples(
|
|
@@ -1271,7 +1280,7 @@ async def splice_add_to_collection(
|
|
|
1271
1280
|
and web UI immediately. Use this to let LivePilot "save for later"
|
|
1272
1281
|
items it finds during composition work.
|
|
1273
1282
|
"""
|
|
1274
|
-
client, err = _require_splice_client(ctx)
|
|
1283
|
+
client, err = await _require_splice_client(ctx)
|
|
1275
1284
|
if err:
|
|
1276
1285
|
return err
|
|
1277
1286
|
if not file_hashes:
|
|
@@ -1289,7 +1298,7 @@ async def splice_remove_from_collection(
|
|
|
1289
1298
|
ctx: Context, collection_uuid: str, file_hashes: list[str],
|
|
1290
1299
|
) -> dict:
|
|
1291
1300
|
"""Remove one or more samples from a user Collection (server-side)."""
|
|
1292
|
-
client, err = _require_splice_client(ctx)
|
|
1301
|
+
client, err = await _require_splice_client(ctx)
|
|
1293
1302
|
if err:
|
|
1294
1303
|
return err
|
|
1295
1304
|
if not file_hashes:
|
|
@@ -1307,7 +1316,7 @@ async def splice_remove_from_collection(
|
|
|
1307
1316
|
@mcp.tool()
|
|
1308
1317
|
async def splice_create_collection(ctx: Context, name: str) -> dict:
|
|
1309
1318
|
"""Create a new user Collection. Returns the new UUID on success."""
|
|
1310
|
-
client, err = _require_splice_client(ctx)
|
|
1319
|
+
client, err = await _require_splice_client(ctx)
|
|
1311
1320
|
if err:
|
|
1312
1321
|
return err
|
|
1313
1322
|
name = (name or "").strip()
|
|
@@ -1347,7 +1356,7 @@ async def splice_list_presets(
|
|
|
1347
1356
|
],
|
|
1348
1357
|
}
|
|
1349
1358
|
"""
|
|
1350
|
-
client, err = _require_splice_client(ctx)
|
|
1359
|
+
client, err = await _require_splice_client(ctx)
|
|
1351
1360
|
if err:
|
|
1352
1361
|
return err
|
|
1353
1362
|
total, presets = await client.list_purchased_presets(
|
|
@@ -1371,7 +1380,7 @@ async def splice_preset_info(
|
|
|
1371
1380
|
plugin_name: str = "",
|
|
1372
1381
|
) -> dict:
|
|
1373
1382
|
"""Fetch metadata for a single preset (uuid, file_hash, or plugin_name)."""
|
|
1374
|
-
client, err = _require_splice_client(ctx)
|
|
1383
|
+
client, err = await _require_splice_client(ctx)
|
|
1375
1384
|
if err:
|
|
1376
1385
|
return err
|
|
1377
1386
|
if not (uuid or file_hash or plugin_name):
|
|
@@ -1394,7 +1403,7 @@ async def splice_download_preset(ctx: Context, uuid: str) -> dict:
|
|
|
1394
1403
|
"""
|
|
1395
1404
|
from ..splice_client.client import CREDIT_HARD_FLOOR
|
|
1396
1405
|
|
|
1397
|
-
client, err = _require_splice_client(ctx)
|
|
1406
|
+
client, err = await _require_splice_client(ctx)
|
|
1398
1407
|
if err:
|
|
1399
1408
|
return err
|
|
1400
1409
|
if not uuid:
|
|
@@ -1437,7 +1446,7 @@ async def splice_pack_info(ctx: Context, pack_uuid: str) -> dict:
|
|
|
1437
1446
|
Useful for discovering related samples by pack, or surfacing pack-level
|
|
1438
1447
|
genre/provider info that search results omit.
|
|
1439
1448
|
"""
|
|
1440
|
-
client, err = _require_splice_client(ctx)
|
|
1449
|
+
client, err = await _require_splice_client(ctx)
|
|
1441
1450
|
if err:
|
|
1442
1451
|
return err
|
|
1443
1452
|
if not pack_uuid:
|
|
@@ -1469,19 +1478,15 @@ async def splice_pack_info(ctx: Context, pack_uuid: str) -> dict:
|
|
|
1469
1478
|
# ────────────────────────────────────────────────────────────────────────
|
|
1470
1479
|
|
|
1471
1480
|
|
|
1472
|
-
def _build_http_bridge(ctx: Context):
|
|
1481
|
+
async def _build_http_bridge(ctx: Context):
|
|
1473
1482
|
"""Construct the HTTPS bridge with the current gRPC client attached.
|
|
1474
1483
|
|
|
1475
1484
|
Returns (bridge, err_dict). On success err_dict is None.
|
|
1476
1485
|
"""
|
|
1477
1486
|
from ..splice_client.http_bridge import SpliceHTTPBridge
|
|
1478
1487
|
|
|
1479
|
-
client =
|
|
1480
|
-
|
|
1481
|
-
client = ctx.lifespan_context.get("splice_client")
|
|
1482
|
-
except AttributeError:
|
|
1483
|
-
pass
|
|
1484
|
-
if client is None or not getattr(client, "connected", False):
|
|
1488
|
+
client = await _ensure_splice_client_connected(ctx)
|
|
1489
|
+
if client is None:
|
|
1485
1490
|
return None, {
|
|
1486
1491
|
"ok": False,
|
|
1487
1492
|
"error": "Splice gRPC not connected — session token unreachable",
|
|
@@ -1496,25 +1501,31 @@ async def splice_describe_sound(
|
|
|
1496
1501
|
bpm: Optional[int] = None,
|
|
1497
1502
|
key: Optional[str] = None,
|
|
1498
1503
|
limit: int = 20,
|
|
1504
|
+
rephrase: bool = True,
|
|
1499
1505
|
) -> dict:
|
|
1500
1506
|
"""Natural-language sample search — the Sounds Plugin's "Describe a Sound".
|
|
1501
1507
|
|
|
1502
1508
|
Splice's AI matches free-form descriptions like "dark ambient pad with
|
|
1503
|
-
shimmer" or "tight 90s house hi-hat" to catalog samples.
|
|
1504
|
-
|
|
1505
|
-
|
|
1509
|
+
shimmer" or "tight 90s house hi-hat" to catalog samples. Hits the
|
|
1510
|
+
GraphQL `SamplesSearch` operation on `surfaces-graphql.splice.com`
|
|
1511
|
+
with `semantic=1` + `rephrase=true` enabled.
|
|
1506
1512
|
|
|
1507
|
-
**Status:
|
|
1508
|
-
|
|
1509
|
-
`SPLICE_ALLOW_UNVERIFIED_ENDPOINTS=1`), this tool returns a structured
|
|
1510
|
-
ENDPOINT_NOT_CONFIGURED error with actionable setup steps.
|
|
1513
|
+
**Status: LIVE** as of 2026-04-22. Endpoint captured via mitmproxy
|
|
1514
|
+
against Splice desktop 5.4.9 + Sounds Plugin.
|
|
1511
1515
|
|
|
1512
1516
|
description: free-text prompt ("warm analog bass under 80bpm")
|
|
1513
1517
|
bpm: optional BPM filter
|
|
1514
1518
|
key: optional musical key ("Dm", "F#")
|
|
1515
1519
|
limit: max results (default 20)
|
|
1520
|
+
rephrase: let Splice's ML rephrase the query for better matches
|
|
1521
|
+
(default True). Returned as `rephrased_query_string`.
|
|
1522
|
+
|
|
1523
|
+
Returns `{ok, query, samples[], total_hits, rephrased_query_string,
|
|
1524
|
+
tag_summary[], ...}`. Each sample has uuid/name/bpm/key/duration/
|
|
1525
|
+
instrument/tags/pack_name/files. Use the uuid with
|
|
1526
|
+
`splice_download_sample(uuid)` to pull the audio file.
|
|
1516
1527
|
"""
|
|
1517
|
-
bridge, err = _build_http_bridge(ctx)
|
|
1528
|
+
bridge, err = await _build_http_bridge(ctx)
|
|
1518
1529
|
if err:
|
|
1519
1530
|
return err
|
|
1520
1531
|
from ..splice_client.http_bridge import SpliceHTTPError
|
|
@@ -1524,95 +1535,157 @@ async def splice_describe_sound(
|
|
|
1524
1535
|
result = await bridge.describe_sound(
|
|
1525
1536
|
description=description.strip(),
|
|
1526
1537
|
bpm=bpm, key=key, limit=int(limit),
|
|
1538
|
+
rephrase=bool(rephrase),
|
|
1527
1539
|
)
|
|
1528
1540
|
except SpliceHTTPError as exc:
|
|
1529
1541
|
return exc.to_dict()
|
|
1530
1542
|
except Exception as exc:
|
|
1531
1543
|
return {"ok": False, "error": f"describe_sound failed: {exc}"}
|
|
1532
|
-
|
|
1544
|
+
# Don't expose the full GraphQL `raw` dict in the user-facing response
|
|
1545
|
+
# unless they asked — it adds ~270KB noise per call. Keep it for
|
|
1546
|
+
# power users via an explicit future flag.
|
|
1547
|
+
out = dict(result) if isinstance(result, dict) else {"raw": result}
|
|
1548
|
+
out.pop("raw", None)
|
|
1549
|
+
return {"ok": True, "query": description, **out}
|
|
1533
1550
|
|
|
1534
1551
|
|
|
1535
1552
|
@mcp.tool()
|
|
1536
1553
|
async def splice_generate_variation(
|
|
1537
1554
|
ctx: Context,
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
target_bpm: Optional[int] = None,
|
|
1541
|
-
count: int = 1,
|
|
1555
|
+
uuid: str,
|
|
1556
|
+
is_legacy: bool = True,
|
|
1542
1557
|
) -> dict:
|
|
1543
|
-
"""
|
|
1544
|
-
|
|
1545
|
-
Splice's
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1558
|
+
"""Find catalog samples similar to a given Splice sample — the "Variations" feature.
|
|
1559
|
+
|
|
1560
|
+
Splice's right-click "Variations" menu item surfaces other catalog
|
|
1561
|
+
samples with similar sonic character. The GraphQL operation name
|
|
1562
|
+
is `AssetSimilarSoundsQuery`. Up to 10 results per call. No credit
|
|
1563
|
+
cost (this is a recommender lookup, not AI audio synthesis — the
|
|
1564
|
+
original naming in the handoff was aspirational).
|
|
1565
|
+
|
|
1566
|
+
**Status: LIVE** as of 2026-04-22. Endpoint captured via mitmproxy
|
|
1567
|
+
against Splice desktop v5.4.9.
|
|
1568
|
+
|
|
1569
|
+
uuid: source sample's catalog uuid (from `splice_describe_sound`
|
|
1570
|
+
results or any other Splice metadata call)
|
|
1571
|
+
is_legacy: match how Splice's own client sets it — default True is
|
|
1572
|
+
correct for all mainstream catalog samples; set False only
|
|
1573
|
+
if working with post-catalog-v2 assets
|
|
1574
|
+
|
|
1575
|
+
Returns `{ok, uuid, similar_samples[], count}`. Each entry has the
|
|
1576
|
+
same flat shape as a describe_sound sample (uuid/name/bpm/key/
|
|
1577
|
+
duration/tags/pack_name/files). Use the uuid of any result with
|
|
1578
|
+
`splice_download_sample()` to pull the audio.
|
|
1561
1579
|
"""
|
|
1562
|
-
bridge, err = _build_http_bridge(ctx)
|
|
1580
|
+
bridge, err = await _build_http_bridge(ctx)
|
|
1563
1581
|
if err:
|
|
1564
1582
|
return err
|
|
1565
1583
|
from ..splice_client.http_bridge import SpliceHTTPError
|
|
1566
|
-
if not
|
|
1567
|
-
return {"ok": False, "error": "
|
|
1568
|
-
if count < 1 or count > 5:
|
|
1569
|
-
return {"ok": False, "error": "count must be 1-5"}
|
|
1584
|
+
if not uuid or not uuid.strip():
|
|
1585
|
+
return {"ok": False, "error": "uuid is required"}
|
|
1570
1586
|
try:
|
|
1571
1587
|
result = await bridge.generate_variation(
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
target_bpm=target_bpm,
|
|
1575
|
-
count=int(count),
|
|
1588
|
+
uuid=uuid.strip(),
|
|
1589
|
+
is_legacy=bool(is_legacy),
|
|
1576
1590
|
)
|
|
1577
1591
|
except SpliceHTTPError as exc:
|
|
1578
1592
|
return exc.to_dict()
|
|
1579
1593
|
except Exception as exc:
|
|
1580
1594
|
return {"ok": False, "error": f"generate_variation failed: {exc}"}
|
|
1581
|
-
|
|
1595
|
+
out = dict(result) if isinstance(result, dict) else {"raw": result}
|
|
1596
|
+
out.pop("raw", None) # drop verbose debug payload
|
|
1597
|
+
return {"ok": True, "uuid": uuid, **out}
|
|
1582
1598
|
|
|
1583
1599
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
limit: int = 20,
|
|
1589
|
-
) -> dict:
|
|
1590
|
-
"""Reference-audio search — the Sounds Plugin's "Search with Sound".
|
|
1600
|
+
# NOTE: splice_search_with_sound was removed 2026-04-22 — user does this
|
|
1601
|
+
# in-Splice manually. If someone wants to resurrect it, the capture recipe
|
|
1602
|
+
# is still at docs/2026-04-22-splice-https-capture-recipe.md.
|
|
1603
|
+
|
|
1591
1604
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
and
|
|
1605
|
+
@mcp.tool()
|
|
1606
|
+
async def splice_http_diagnose(ctx: Context) -> dict:
|
|
1607
|
+
"""Diagnose the Splice HTTPS bridge configuration and readiness.
|
|
1595
1608
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
NOT_YET_IMPLEMENTED error.
|
|
1609
|
+
Reports which endpoints are configured, whether a session token is
|
|
1610
|
+
reachable from the gRPC client, and what the next step is to unblock
|
|
1611
|
+
`splice_describe_sound` and `splice_generate_variation`.
|
|
1600
1612
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1613
|
+
Use this BEFORE calling either tool if you want a clear readout of
|
|
1614
|
+
"what's missing, and how do I fix it" instead of per-tool
|
|
1615
|
+
ENDPOINT_NOT_CONFIGURED errors.
|
|
1603
1616
|
"""
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1617
|
+
from ..splice_client.http_bridge import SpliceHTTPConfig
|
|
1618
|
+
|
|
1619
|
+
cfg = SpliceHTTPConfig.from_env()
|
|
1620
|
+
endpoints = {
|
|
1621
|
+
"describe": cfg.describe_endpoint,
|
|
1622
|
+
"variation": cfg.variation_endpoint,
|
|
1623
|
+
}
|
|
1624
|
+
verified = {
|
|
1625
|
+
"describe": cfg.describe_verified,
|
|
1626
|
+
"variation": cfg.variation_verified,
|
|
1627
|
+
}
|
|
1628
|
+
unverified = [name for name, ok in verified.items() if not ok]
|
|
1629
|
+
configured_count = sum(1 for v in endpoints.values() if v not in (None, ""))
|
|
1630
|
+
|
|
1631
|
+
# Try to read the session token via the gRPC client the SAME way
|
|
1632
|
+
# the real tools do — reach into ctx.lifespan_context["splice_client"]
|
|
1633
|
+
# and actually attempt a GetSession fetch. Walking a different
|
|
1634
|
+
# engine-nested path (earlier mistake) reported "token unavailable"
|
|
1635
|
+
# while the bridge's real request path succeeded — a misleading
|
|
1636
|
+
# diagnostic is worse than no diagnostic.
|
|
1637
|
+
session_token_available = False
|
|
1638
|
+
session_token_error = None
|
|
1639
|
+
grpc_client = await _ensure_splice_client_connected(ctx)
|
|
1640
|
+
if grpc_client is None:
|
|
1641
|
+
session_token_error = "Splice gRPC not connected"
|
|
1642
|
+
else:
|
|
1643
|
+
# Connection is up; confirm a token actually comes back.
|
|
1644
|
+
from ..splice_client.http_bridge import fetch_session_token
|
|
1645
|
+
try:
|
|
1646
|
+
token = await fetch_session_token(grpc_client)
|
|
1647
|
+
if token:
|
|
1648
|
+
session_token_available = True
|
|
1649
|
+
else:
|
|
1650
|
+
session_token_error = (
|
|
1651
|
+
"GetSession RPC returned no token — user may be "
|
|
1652
|
+
"logged out or gRPC schema drifted"
|
|
1653
|
+
)
|
|
1654
|
+
except Exception as exc:
|
|
1655
|
+
session_token_error = f"GetSession call failed: {exc}"
|
|
1656
|
+
|
|
1657
|
+
next_steps: list = []
|
|
1658
|
+
if "describe" in unverified:
|
|
1659
|
+
next_steps.append(
|
|
1660
|
+
"Describe endpoint unverified — reset config to defaults "
|
|
1661
|
+
"(delete ~/.livepilot/splice.json or unset env vars) so the "
|
|
1662
|
+
"captured surfaces-graphql.splice.com/graphql endpoint is used."
|
|
1613
1663
|
)
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1664
|
+
if "variation" in unverified:
|
|
1665
|
+
next_steps.append(
|
|
1666
|
+
"Variation GraphQL operation not yet captured. Right-click "
|
|
1667
|
+
"a Splice sample and click Variations with mitmproxy running. "
|
|
1668
|
+
"See docs/2026-04-22-splice-https-capture-recipe.md."
|
|
1669
|
+
)
|
|
1670
|
+
if not session_token_available:
|
|
1671
|
+
next_steps.append(
|
|
1672
|
+
"Splice desktop app is not reachable — the bridge reads the "
|
|
1673
|
+
"session token via gRPC GetSession RPC. Ensure the app is "
|
|
1674
|
+
"running and logged in."
|
|
1675
|
+
)
|
|
1676
|
+
if not next_steps:
|
|
1677
|
+
next_steps.append("Bridge fully ready — test with splice_describe_sound.")
|
|
1678
|
+
|
|
1679
|
+
return {
|
|
1680
|
+
"ok": True,
|
|
1681
|
+
"base_url": cfg.base_url,
|
|
1682
|
+
"endpoints": endpoints,
|
|
1683
|
+
"verified": verified,
|
|
1684
|
+
"configured_count": configured_count,
|
|
1685
|
+
"unverified_endpoints": unverified,
|
|
1686
|
+
"is_user_configured": cfg.is_user_configured,
|
|
1687
|
+
"session_token_available": session_token_available,
|
|
1688
|
+
"session_token_error": session_token_error,
|
|
1689
|
+
"next_steps": next_steps,
|
|
1690
|
+
"docs": "docs/2026-04-22-splice-https-capture-recipe.md",
|
|
1691
|
+
}
|