3939
4040import asyncio
4141import json
42+ import os
4243from typing import Any
44+ from urllib .parse import urlparse
4345
4446import websockets
4547from mcp .server .fastmcp import FastMCP
5153class WorkflowConnection :
5254 """Holds WebSocket connection and buffered state for one DPL workflow."""
5355
54- def __init__ (self , port : int , name : str ):
55- self .port = port
56+ def __init__ (self , * , url : str , name : str , extra_headers : dict [ str , str ] | None = None ):
57+ self .url = url
5658 self .name = name
59+ self .extra_headers = extra_headers or {}
5760 self .ws : Any = None
5861 self .reader_task : asyncio .Task | None = None
5962 self .snapshot : dict = {}
@@ -83,8 +86,11 @@ async def ensure_connected(self) -> None:
8386 except Exception :
8487 pass
8588
86- url = f"ws://localhost:{ self .port } /status"
87- self .ws = await websockets .connect (url , subprotocols = ["dpl" ])
89+ self .ws = await websockets .connect (
90+ self .url ,
91+ subprotocols = ["dpl" ],
92+ additional_headers = self .extra_headers if self .extra_headers else None ,
93+ )
8894 if self .reader_task is None or self .reader_task .done ():
8995 self .reader_task = asyncio .create_task (self ._reader ())
9096
@@ -178,7 +184,8 @@ async def connect(port: int = 0, pid: int = 0, name: str = "") -> str:
178184 old = _workflows [wf_name ]
179185 await old .close ()
180186
181- conn = WorkflowConnection (port , wf_name )
187+ url = f"ws://localhost:{ port } /status"
188+ conn = WorkflowConnection (url = url , name = wf_name )
182189 await conn .ensure_connected ()
183190 _workflows [wf_name ] = conn
184191
@@ -189,6 +196,48 @@ async def connect(port: int = 0, pid: int = 0, name: str = "") -> str:
189196 )
190197
191198
199+ @mcp .tool ()
200+ async def connect_hyperloop (url : str , name : str = "" , token : str = "" ) -> str :
201+ """Connect to a DPL workflow running on Hyperloop via the remote proxy.
202+
203+ Accepts a URL like:
204+ https://alimonitor.cern.ch/train-workdir/remote-gui/remote_proxy.html?<token>/<port>
205+
206+ and remaps it to the local WebSocket proxy endpoint.
207+
208+ Args:
209+ url: The remote_proxy.html URL from alimonitor.
210+ name: Optional human-friendly name for this workflow.
211+ token: Hyperloop auth token. Falls back to HYPERLOOP_TOKEN env var.
212+ """
213+ token = token or os .environ .get ("HYPERLOOP_TOKEN" , "" )
214+ if not token :
215+ return "No token provided and HYPERLOOP_TOKEN environment variable is not set."
216+
217+ parsed = urlparse (url )
218+ path_suffix = parsed .query # everything after '?'
219+ if not path_suffix :
220+ return f"Cannot parse token/port from URL: { url } "
221+
222+ ws_url = f"ws://localhost:8888/remote-mcp/o2/{ path_suffix } /status"
223+ wf_name = name or path_suffix .split ("/" )[- 1 ]
224+
225+ if wf_name in _workflows :
226+ old = _workflows [wf_name ]
227+ await old .close ()
228+
229+ headers = {"Authorization" : f"Bearer { token } " }
230+ conn = WorkflowConnection (url = ws_url , name = wf_name , extra_headers = headers )
231+ await conn .ensure_connected ()
232+ _workflows [wf_name ] = conn
233+
234+ devices = conn .snapshot .get ("devices" , [])
235+ return (
236+ f"Connected to Hyperloop workflow '{ wf_name } ' via { ws_url } "
237+ f"({ len (devices )} device(s))."
238+ )
239+
240+
192241@mcp .tool ()
193242async def disconnect (workflow : str ) -> str :
194243 """Disconnect from a DPL workflow and release its resources.
0 commit comments