Skip to content

Commit 864e705

Browse files
committed
DPL MCP: allow connecting to a running Hyperloop test
1 parent 3ee5c9f commit 864e705

1 file changed

Lines changed: 54 additions & 5 deletions

File tree

Framework/Core/scripts/dpl-mcp-server/dpl_mcp_server.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939

4040
import asyncio
4141
import json
42+
import os
4243
from typing import Any
44+
from urllib.parse import urlparse
4345

4446
import websockets
4547
from mcp.server.fastmcp import FastMCP
@@ -51,9 +53,10 @@
5153
class 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()
193242
async def disconnect(workflow: str) -> str:
194243
"""Disconnect from a DPL workflow and release its resources.

0 commit comments

Comments
 (0)