Skip to content

Commit e321fbb

Browse files
fix: expose MuJoCoViewerServer proxy for integration tests
1 parent dc95916 commit e321fbb

1 file changed

Lines changed: 89 additions & 33 deletions

File tree

src/mujoco_mcp/viewer_server.py

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,100 @@
1-
#!/usr/bin/env python3
2-
"""
3-
MuJoCo Viewer Server Launcher
4-
Simplified viewer server for package distribution
1+
"""MuJoCo Viewer Server access helpers.
2+
3+
This module exposes the interactive viewer server class used by integration and
4+
RL tests while keeping a simple CLI entry point for package builds. The actual
5+
implementation lives in the top-level ``mujoco_viewer_server`` module so that it
6+
can be launched both from source checkouts and from installed environments.
57
"""
68

7-
import sys
8-
import subprocess
9+
from __future__ import annotations
10+
11+
import importlib
912
import logging
13+
import subprocess
14+
import sys
1015
from pathlib import Path
16+
from types import ModuleType
17+
from typing import Optional
1118

12-
def main():
13-
"""Main entry point for viewer server"""
14-
logging.basicConfig(level=logging.INFO)
15-
logger = logging.getLogger("mujoco-mcp-viewer")
19+
_LOGGER = logging.getLogger("mujoco_mcp.viewer_server")
1620

17-
# Find the viewer server script
18-
script_path = Path(__file__).parent.parent.parent / "mujoco_viewer_server.py"
1921

20-
if not script_path.exists():
21-
# Try relative to current working directory
22-
script_path = Path("mujoco_viewer_server.py")
22+
def _load_viewer_module() -> ModuleType:
23+
"""Return the runtime viewer server module, importing it on demand.
2324
24-
if not script_path.exists():
25-
logger.error("Could not find mujoco_viewer_server.py")
26-
logger.error("Please run from the mujoco-mcp directory or ensure the viewer server is in your PATH")
27-
sys.exit(1)
25+
The viewer server lives at the repository root (``mujoco_viewer_server.py``).
26+
When the package is installed in editable mode this file sits alongside the
27+
package sources, so we extend ``sys.path`` with the project root before
28+
importing. If the module cannot be imported we surface a helpful error message
29+
instead of failing silently.
30+
"""
2831

29-
logger.info(f"Starting MuJoCo Viewer Server from {script_path}")
32+
root = Path(__file__).resolve().parents[2]
33+
if str(root) not in sys.path:
34+
sys.path.append(str(root))
3035

3136
try:
32-
# Launch the viewer server
33-
subprocess.run([sys.executable, str(script_path)], check=True)
34-
except KeyboardInterrupt:
35-
logger.info("Viewer server stopped by user")
36-
except subprocess.CalledProcessError as e:
37-
logger.exception(f"Viewer server failed: {e}")
38-
sys.exit(1)
39-
except Exception as e:
40-
logger.exception(f"Unexpected error: {e}")
41-
sys.exit(1)
42-
43-
if __name__ == "__main__":
44-
main()
37+
return importlib.import_module("mujoco_viewer_server")
38+
except Exception as exc: # pragma: no cover - executed only when missing deps
39+
raise ImportError(
40+
"Unable to import mujoco_viewer_server. Ensure the viewer server script "
41+
"is available in the project root or installed alongside the package"
42+
) from exc
43+
44+
45+
def get_viewer_class() -> type:
46+
"""Fetch ``MuJoCoViewerServer`` from the runtime module."""
47+
48+
module = _load_viewer_module()
49+
try:
50+
return getattr(module, "MuJoCoViewerServer")
51+
except AttributeError as exc: # pragma: no cover - defensive guard
52+
raise ImportError(
53+
"mujoco_viewer_server does not define MuJoCoViewerServer"
54+
) from exc
55+
56+
57+
class MuJoCoViewerServer(get_viewer_class()): # type: ignore[misc]
58+
"""Proxy subclass so imports continue to work unchanged.
59+
60+
``get_viewer_class`` is evaluated at import time and returns the concrete
61+
implementation. Subclassing keeps backward compatibility for user code that
62+
expects ``mujoco_mcp.viewer_server.MuJoCoViewerServer`` to be instantiable.
63+
"""
64+
65+
pass
66+
67+
68+
def _resolve_script_path() -> Path:
69+
"""Locate the standalone viewer server script used by the CLI."""
70+
71+
module = _load_viewer_module()
72+
path = Path(getattr(module, "__file__", ""))
73+
if not path:
74+
raise FileNotFoundError("Unable to resolve mujoco_viewer_server.py path")
75+
return path
76+
77+
78+
def main(argv: Optional[list[str]] = None) -> int:
79+
"""CLI entry point that spawns the viewer server in a child process."""
80+
81+
logging.basicConfig(level=logging.INFO)
82+
script_path = _resolve_script_path()
83+
cmd = [sys.executable, str(script_path)]
84+
85+
try:
86+
completed = subprocess.run(cmd, check=True)
87+
return completed.returncode
88+
except subprocess.CalledProcessError as exc:
89+
_LOGGER.exception("Viewer server exited with error")
90+
return exc.returncode
91+
except KeyboardInterrupt: # pragma: no cover - interactive convenience
92+
_LOGGER.info("Viewer server interrupted by user")
93+
return 130
94+
except Exception as exc: # pragma: no cover - defensive
95+
_LOGGER.exception("Unexpected viewer server failure: %s", exc)
96+
return 1
97+
98+
99+
if __name__ == "__main__": # pragma: no cover - CLI shim
100+
sys.exit(main())

0 commit comments

Comments
 (0)