@@ -277,6 +277,148 @@ async def fetch_one(wid: str) -> dict | None:
277277 return "\n " .join (lines )
278278
279279
280+ # ---------------------------------------------------------------------------
281+ # Analysis / wagon browsing
282+ #
283+ # These mirror the alihyperloop web UI's analysis pages. Endpoint and param
284+ # names were taken from the frontend bundle (/hyperloop/assets/index-*.js);
285+ # unknown `lists` values silently return an empty array, and the wagon list
286+ # uses the *plural* `analysis_ids`.
287+ # ---------------------------------------------------------------------------
288+
289+
290+ @mcp .tool ()
291+ async def list_analyses (username : str ) -> str :
292+ """List a user's Hyperloop analyses (id, name, JIRA, analyzers).
293+
294+ `username` is the CERN login of an analyzer (e.g. "eulisse").
295+ """
296+ rows = await _get ("analysis/list-analysis.jsp" ,
297+ {"lists" : "analysis-by-username" , "username" : username })
298+ if not isinstance (rows , list ) or not rows :
299+ return f"No analyses found for user '{ username } '."
300+ lines = [f"Analyses for { username } :\n " ,
301+ f"{ 'ID' :>7} { 'Svc' :<3} { 'JIRA' :<14} Name" ]
302+ lines .append ("-" * 70 )
303+ for a in rows :
304+ svc = "yes" if a .get ("service_analysis" ) else ""
305+ lines .append (f"{ a .get ('id' ):>7} { svc :<3} { str (a .get ('jira_id' ) or '' ):<14} "
306+ f"{ a .get ('name' )} " )
307+ return "\n " .join (lines )
308+
309+
310+ @mcp .tool ()
311+ async def analysis_wagons (analysis_id : int ) -> str :
312+ """List the wagons of an analysis (wagon id, name, last test train id)."""
313+ data = await _get ("analysis/wagons-by-analyses.jsp" ,
314+ {"analysis_ids" : analysis_id })
315+ if not isinstance (data , dict ) or not data :
316+ return f"No wagons found for analysis { analysis_id } ."
317+ rows = sorted (data .values (), key = lambda w : str (w .get ("name" , "" )).lower ())
318+ lines = [f"{ len (rows )} wagons in analysis { analysis_id } :\n " ,
319+ f"{ 'WagonID' :>8} { 'TrainID' :>8} Name" ]
320+ lines .append ("-" * 70 )
321+ for w in rows :
322+ lines .append (f"{ w .get ('id' ):>8} { str (w .get ('train_id' ) or '-' ):>8} "
323+ f"{ w .get ('name' )} " )
324+ return "\n " .join (lines )
325+
326+
327+ @mcp .tool ()
328+ async def wagon_config (wagon_id : int , device : str = "" ) -> str :
329+ """Show a wagon's merged configuration (device -> parameters).
330+
331+ If `device` is given, only devices whose name contains that substring are
332+ shown (e.g. "pid-tpc-service"); otherwise the device list + sizes is shown.
333+ """
334+ cfg = await _get ("analysis/wagon/download-configuration.jsp" ,
335+ {"wagon_id" : wagon_id })
336+ if not isinstance (cfg , dict ) or not cfg :
337+ return f"No configuration for wagon { wagon_id } ."
338+ devices = {k : v for k , v in cfg .items () if isinstance (v , dict )}
339+ if not device :
340+ lines = [f"Wagon { wagon_id } : { len (devices )} configured devices:\n " ]
341+ for k in sorted (devices ):
342+ lines .append (f" { k } ({ len (devices [k ])} params)" )
343+ lines .append ("\n Pass device=<substring> to see a device's parameters." )
344+ return "\n " .join (lines )
345+ matched = {k : v for k , v in devices .items () if device in k }
346+ if not matched :
347+ return f"Wagon { wagon_id } : no device matching '{ device } '."
348+ lines = []
349+ for k in sorted (matched ):
350+ lines .append (f"[{ k } ]" )
351+ for p in sorted (matched [k ]):
352+ lines .append (f" { p } = { matched [k ][p ]} " )
353+ lines .append ("" )
354+ return "\n " .join (lines ).rstrip ()
355+
356+
357+ @mcp .tool ()
358+ async def find_wagons_by_config (analysis_id : int , param : str ,
359+ value : str | None = None ) -> str :
360+ """Find wagons in an analysis whose config sets a given parameter.
361+
362+ Scans every wagon's merged config for a device parameter whose name
363+ contains `param` (e.g. "useNetworkCorrection"). If `value` is given, only
364+ wagons where the parameter equals it are reported. Each hit resolves the
365+ wagon's dataset name(s) so Run 2 vs Run 3 is visible.
366+
367+ Example: find wagons running the TPC PID neural network ->
368+ find_wagons_by_config(50446, "pidTPC.useNetworkCorrection", "1")
369+ """
370+ wagons = await _get ("analysis/wagons-by-analyses.jsp" ,
371+ {"analysis_ids" : analysis_id })
372+ if not isinstance (wagons , dict ) or not wagons :
373+ return f"No wagons found for analysis { analysis_id } ."
374+
375+ # wagon_id -> [dataset names], via the wagon<->dataset associations.
376+ assoc = await _get ("analysis/wagondataset-by-analyses.jsp" ,
377+ {"analysis_ids" : analysis_id })
378+ train_ids = {a .get ("test_train_id" ) for a in (assoc or [])
379+ if a .get ("test_train_id" )}
380+ train_ds = {}
381+ for tid in train_ids :
382+ try :
383+ t = await _get ("trains/train.jsp" , {"train_id" : tid })
384+ t = t [0 ] if isinstance (t , list ) else t
385+ train_ds [tid ] = t .get ("dataset_name" )
386+ except Exception :
387+ pass
388+ wagon_ds : dict = {}
389+ for a in (assoc or []):
390+ ds = train_ds .get (a .get ("test_train_id" ))
391+ if ds :
392+ wagon_ds .setdefault (str (a .get ("wagon_id" )), set ()).add (ds )
393+
394+ hits = []
395+ for wid , w in wagons .items ():
396+ try :
397+ cfg = await _get ("analysis/wagon/download-configuration.jsp" ,
398+ {"wagon_id" : wid })
399+ except Exception :
400+ continue
401+ for dev , c in cfg .items () if isinstance (cfg , dict ) else []:
402+ if not isinstance (c , dict ):
403+ continue
404+ for p , v in c .items ():
405+ if param not in p :
406+ continue
407+ if value is not None and str (v ) != str (value ):
408+ continue
409+ ds = ", " .join (sorted (wagon_ds .get (str (wid ), []))) or "?"
410+ hits .append ((w .get ("name" ), wid , dev , p , str (v ), ds ))
411+
412+ if not hits :
413+ cond = f"{ param } ={ value } " if value is not None else param
414+ return f"No wagons in analysis { analysis_id } match { cond } ."
415+ lines = [f"Wagons in analysis { analysis_id } matching '{ param } '"
416+ + (f"={ value } " if value is not None else "" ) + ":\n " ]
417+ for name , wid , dev , p , v , ds in hits :
418+ lines .append (f" { str (name )[:34 ]:34} wagon { wid :>6} | { dev } | { p } ={ v } | { ds } " )
419+ return "\n " .join (lines )
420+
421+
280422def main ():
281423 import argparse
282424 global PROXY , TOKEN , API
0 commit comments