Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ under their entry.
diagrams (local stdio vs HPC dispatch).

### Fixed
- CLI config discovery now honors `./config.yaml` in the current working
directory at priority above the user config, matching the behavior of
git/ruff/pytest. Previously an empty user config written by
`uxarray-mcp setup` would silently shadow the repo's `config.yaml`,
causing `uxarray-mcp endpoints list` to print "No endpoints configured"
even when the project had endpoints defined. The empty-state message
now also names the loaded config file (or every searched path when
none was found) so the discovery is no longer invisible.
- End-to-end validation of `run_scientific_agent` and `analyze_dataset`
on local + Improv + UCAR endpoints. New harness
(`scripts/validate_orchestrators.py`) covers six scenarios; both
Expand Down
11 changes: 10 additions & 1 deletion src/uxarray_mcp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from uxarray_mcp.remote.config import (
USER_CONFIG_PATH,
discover_config_path,
discover_config_search_paths,
load_config,
)

Expand Down Expand Up @@ -118,8 +119,16 @@ def _user_write_target() -> Path:

def cmd_endpoints_list(args: argparse.Namespace) -> int:
cfg = load_config()
loaded = discover_config_path()
if not cfg.endpoints and not cfg.endpoint_id:
print("No endpoints configured.")
if loaded is None:
print("No endpoints configured. No config file was found. Searched:")
for p in discover_config_search_paths():
print(f" - {p}")
print("\nWrite a starter config with: uxarray-mcp setup")
else:
print(f"No endpoints configured in {loaded}.")
print("\nAdd one with: uxarray-mcp endpoints add <name> <uuid>")
return 0
payload: dict[str, Any] = {
"config_path": str(discover_config_path() or ""),
Expand Down
30 changes: 28 additions & 2 deletions src/uxarray_mcp/remote/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ def discover_config_path() -> Path | None:

Order:
1. ``$UXARRAY_MCP_CONFIG`` (explicit override)
2. ``~/.config/uxarray-mcp/config.yaml`` (user install)
3. ``./config.yaml`` (repo-root, dev/clone install)
2. ``./config.yaml`` (current working directory — project-local)
3. ``~/.config/uxarray-mcp/config.yaml`` (user install)
4. ``<repo_root>/config.yaml`` (editable install fallback)

Project-local (cwd) wins over the user config so that running the CLI
from inside a checkout uses the repo's config, even when an empty user
config was previously written by ``uxarray-mcp setup``.

Returns ``None`` when no config file is found.
"""
Expand All @@ -29,6 +34,10 @@ def discover_config_path() -> Path | None:
if candidate.exists():
return candidate

cwd_config = Path.cwd() / "config.yaml"
if cwd_config.exists():
return cwd_config

if USER_CONFIG_PATH.exists():
return USER_CONFIG_PATH

Expand All @@ -39,6 +48,23 @@ def discover_config_path() -> Path | None:
return None


def discover_config_search_paths() -> list[Path]:
"""Return the ordered list of paths discover_config_path inspects.

Useful for diagnostics — ``endpoints list`` prints this when no
endpoints are configured so the user can see exactly which file was
used or which paths were searched.
"""
paths: list[Path] = []
env_path = os.environ.get("UXARRAY_MCP_CONFIG")
if env_path:
paths.append(Path(env_path).expanduser())
paths.append(Path.cwd() / "config.yaml")
paths.append(USER_CONFIG_PATH)
paths.append(Path(__file__).resolve().parent.parent.parent.parent / "config.yaml")
return paths


_VALID_EXECUTION_MODES = {"local", "hpc", "auto"}
_EXECUTION_MODE_ALIASES = {"remote": "hpc"}

Expand Down
32 changes: 29 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,39 @@ def test_discover_config_uses_env_var(tmp_path, monkeypatch):
assert found == target


def test_discover_config_user_home_beats_repo(tmp_path, monkeypatch):
def test_discover_config_cwd_beats_user_home(tmp_path, monkeypatch):
"""A project-local ./config.yaml takes precedence over the user config.

This prevents ``uxarray-mcp setup`` (which writes an empty user config)
from silently shadowing the repo's own config when the CLI is run from
inside a checkout.
"""
monkeypatch.delenv("UXARRAY_MCP_CONFIG", raising=False)
user_cfg = tmp_path / "user.yaml"
user_cfg.write_text("hpc: {}\n")
monkeypatch.setattr(config_module, "USER_CONFIG_PATH", user_cfg)
found = config_module.discover_config_path()
assert found == user_cfg

cwd_dir = tmp_path / "project"
cwd_dir.mkdir()
cwd_cfg = cwd_dir / "config.yaml"
cwd_cfg.write_text("hpc: {execution_mode: auto}\n")
monkeypatch.chdir(cwd_dir)

assert config_module.discover_config_path() == cwd_cfg


def test_discover_config_falls_back_to_user_home(tmp_path, monkeypatch):
"""When no ./config.yaml exists, fall back to the user config."""
monkeypatch.delenv("UXARRAY_MCP_CONFIG", raising=False)
user_cfg = tmp_path / "user.yaml"
user_cfg.write_text("hpc: {}\n")
monkeypatch.setattr(config_module, "USER_CONFIG_PATH", user_cfg)

empty_dir = tmp_path / "empty"
empty_dir.mkdir()
monkeypatch.chdir(empty_dir)

assert config_module.discover_config_path() == user_cfg


def test_discover_config_no_match_returns_none(tmp_path, monkeypatch):
Expand Down
Loading