diff --git a/CHANGELOG.md b/CHANGELOG.md index be863b7..ef10b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/uxarray_mcp/cli.py b/src/uxarray_mcp/cli.py index 3246dca..493a2bc 100644 --- a/src/uxarray_mcp/cli.py +++ b/src/uxarray_mcp/cli.py @@ -26,6 +26,7 @@ from uxarray_mcp.remote.config import ( USER_CONFIG_PATH, discover_config_path, + discover_config_search_paths, load_config, ) @@ -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 ") return 0 payload: dict[str, Any] = { "config_path": str(discover_config_path() or ""), diff --git a/src/uxarray_mcp/remote/config.py b/src/uxarray_mcp/remote/config.py index 9c16c65..13897b2 100644 --- a/src/uxarray_mcp/remote/config.py +++ b/src/uxarray_mcp/remote/config.py @@ -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. ``/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. """ @@ -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 @@ -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"} diff --git a/tests/test_cli.py b/tests/test_cli.py index 0ebfbec..638541c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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):