Skip to content

Commit c3122dd

Browse files
committed
Make MCP server URL configurable via env var and CLI flag
- Add environment variable overrides in config loader: MAS_AVIARY_MCP_URL, MAS_AVIARY_MCP_MODE, MAS_AVIARY_MODEL_ID, MAS_AVIARY_API_BASE (highest priority, overrides YAML values) - Add --mcp-url flag to stat_batch_runner.py - Document configuration hierarchy in README (CLI > env var > YAML) - Add 6 tests for env var override behavior
1 parent be2a498 commit c3122dd

4 files changed

Lines changed: 152 additions & 5 deletions

File tree

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,46 @@ python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}, de
140140

141141
---
142142

143+
## MCP Server Configuration
144+
145+
The MCP server URL is configurable at three levels (highest priority wins):
146+
147+
1. **CLI flag**: `--mcp-url http://your-host:port/mcp`
148+
2. **Environment variable**: `export MAS_AVIARY_MCP_URL=http://your-host:port/mcp`
149+
3. **YAML config**: edit `mcp.servers[0].url` in `config/aviary_run.yaml`
150+
151+
The default config ships with `http://127.0.0.1:8600/mcp`. Override it for
152+
remote servers, non-standard ports, or containerized deployments:
153+
154+
```bash
155+
# Environment variable (persists for the shell session)
156+
export MAS_AVIARY_MCP_URL=http://192.168.1.50:9000/mcp
157+
158+
# Or pass directly to the runner
159+
python scripts/stat_batch_runner.py --mcp-url http://192.168.1.50:9000/mcp --repeats 1
160+
```
161+
162+
Other environment variable overrides:
163+
164+
| Variable | Overrides | Example |
165+
|----------|-----------|---------|
166+
| `MAS_AVIARY_MCP_URL` | MCP server URL | `http://10.0.0.5:8600/mcp` |
167+
| `MAS_AVIARY_MCP_MODE` | MCP mode (`mock` or `real`) | `mock` |
168+
| `MAS_AVIARY_MODEL_ID` | LLM model | `Qwen/Qwen3-4B` |
169+
| `MAS_AVIARY_API_BASE` | vLLM API endpoint | `http://localhost:8000/v1` |
170+
171+
---
172+
143173
## Quickstart
144174

145175
### 1. Start the Aviary MCP server
146176

147177
The MCP server provides tool-based access to NASA OpenMDAO/Aviary simulations.
148-
It must be running on port 8600 before launching agents:
178+
It must be running before launching agents:
149179

150180
```bash
151181
# See your Aviary MCP server documentation for startup instructions
152-
# The server should be accessible at http://127.0.0.1:8600/mcp
182+
# Default: http://127.0.0.1:8600/mcp (configurable — see above)
153183
```
154184

155185
### 2. Run a single optimization

scripts/stat_batch_runner.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import gc
3434
import json
3535
import math
36+
import os
3637
import time
3738
from dataclasses import dataclass
3839
from pathlib import Path
@@ -835,8 +836,16 @@ def main():
835836
"--timeout", type=float, default=_DEFAULT_TIMEOUT_MINUTES,
836837
help=f"Per-run timeout in minutes (default: {_DEFAULT_TIMEOUT_MINUTES})",
837838
)
839+
parser.add_argument(
840+
"--mcp-url", type=str, default=None,
841+
help="MCP server URL (overrides config; e.g. http://127.0.0.1:8600/mcp)",
842+
)
838843
args = parser.parse_args()
839844

845+
# Allow --mcp-url to override via environment variable.
846+
if args.mcp_url:
847+
os.environ["MAS_AVIARY_MCP_URL"] = args.mcp_url
848+
840849
run_stat_batch(
841850
n_repeats=args.repeats,
842851
combo_names=args.combinations,

src/config/loader.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Configuration loader — reads YAML files and returns structured config."""
22

3+
import os
34
from dataclasses import dataclass, field
45
from pathlib import Path
56
from typing import Any
@@ -86,8 +87,42 @@ def _dict_to_dataclass(cls, data: dict[str, Any]):
8687
return cls(**filtered)
8788

8889

90+
def _apply_env_overrides(config: AppConfig) -> AppConfig:
91+
"""Override config values from environment variables.
92+
93+
Supported variables:
94+
MAS_AVIARY_MCP_URL — overrides the URL of the first MCP server
95+
MAS_AVIARY_MCP_MODE — overrides mcp.mode ("mock" or "real")
96+
MAS_AVIARY_MODEL_ID — overrides llm.model_id
97+
MAS_AVIARY_API_BASE — overrides llm.api_base (for vLLM)
98+
"""
99+
mcp_url = os.environ.get("MAS_AVIARY_MCP_URL")
100+
if mcp_url:
101+
if config.mcp.servers:
102+
config.mcp.servers[0].url = mcp_url
103+
else:
104+
config.mcp.servers = [MCPServerConfig(url=mcp_url)]
105+
106+
mcp_mode = os.environ.get("MAS_AVIARY_MCP_MODE")
107+
if mcp_mode:
108+
config.mcp.mode = mcp_mode
109+
110+
model_id = os.environ.get("MAS_AVIARY_MODEL_ID")
111+
if model_id:
112+
config.llm.model_id = model_id
113+
114+
api_base = os.environ.get("MAS_AVIARY_API_BASE")
115+
if api_base:
116+
config.llm.api_base = api_base
117+
118+
return config
119+
120+
89121
def load_config(path: str | Path) -> AppConfig:
90-
"""Load configuration from a YAML file and return an AppConfig dataclass."""
122+
"""Load configuration from a YAML file and return an AppConfig dataclass.
123+
124+
Environment variables override YAML values (see _apply_env_overrides).
125+
"""
91126
path = Path(path)
92127
if not path.exists():
93128
raise FileNotFoundError(f"Config file not found: {path}")
@@ -96,9 +131,11 @@ def load_config(path: str | Path) -> AppConfig:
96131
raw = yaml.safe_load(f)
97132

98133
if raw is None:
99-
return AppConfig()
134+
config = AppConfig()
135+
else:
136+
config = _dict_to_dataclass(AppConfig, raw)
100137

101-
return _dict_to_dataclass(AppConfig, raw)
138+
return _apply_env_overrides(config)
102139

103140

104141
def load_yaml(path: str | Path) -> dict[str, Any]:

tests/test_config_loader.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for the configuration loader."""
22

3+
import os
4+
35
import pytest
46
import yaml
57

@@ -172,3 +174,72 @@ def test_appconfig_defaults():
172174
assert isinstance(cfg.logging, LoggingConfig)
173175
assert isinstance(cfg.ui, UIConfig)
174176
assert cfg.agents_config == "config/agents.yaml"
177+
178+
179+
# ---- environment variable overrides -------------------------------------------
180+
181+
def test_env_override_mcp_url(tmp_path, monkeypatch):
182+
"""MAS_AVIARY_MCP_URL overrides the first MCP server URL."""
183+
cfg_file = tmp_path / "test.yaml"
184+
cfg_file.write_text(yaml.dump({
185+
"mcp": {
186+
"mode": "real",
187+
"servers": [{"url": "http://original:8600/mcp", "transport": "streamable-http"}],
188+
}
189+
}))
190+
monkeypatch.setenv("MAS_AVIARY_MCP_URL", "http://custom-host:9999/mcp")
191+
cfg = load_config(str(cfg_file))
192+
assert cfg.mcp.servers[0].url == "http://custom-host:9999/mcp"
193+
194+
195+
def test_env_override_mcp_url_creates_server(tmp_path, monkeypatch):
196+
"""MAS_AVIARY_MCP_URL creates a server entry if none exist."""
197+
cfg_file = tmp_path / "empty_mcp.yaml"
198+
cfg_file.write_text(yaml.dump({"mcp": {"mode": "real"}}))
199+
monkeypatch.setenv("MAS_AVIARY_MCP_URL", "http://new-host:7000/mcp")
200+
cfg = load_config(str(cfg_file))
201+
assert len(cfg.mcp.servers) == 1
202+
assert cfg.mcp.servers[0].url == "http://new-host:7000/mcp"
203+
204+
205+
def test_env_override_mcp_mode(tmp_path, monkeypatch):
206+
"""MAS_AVIARY_MCP_MODE overrides mcp.mode."""
207+
cfg_file = tmp_path / "test.yaml"
208+
cfg_file.write_text(yaml.dump({"mcp": {"mode": "real"}}))
209+
monkeypatch.setenv("MAS_AVIARY_MCP_MODE", "mock")
210+
cfg = load_config(str(cfg_file))
211+
assert cfg.mcp.mode == "mock"
212+
213+
214+
def test_env_override_model_id(tmp_path, monkeypatch):
215+
"""MAS_AVIARY_MODEL_ID overrides llm.model_id."""
216+
cfg_file = tmp_path / "test.yaml"
217+
cfg_file.write_text(yaml.dump({"llm": {"model_id": "original-model"}}))
218+
monkeypatch.setenv("MAS_AVIARY_MODEL_ID", "Qwen/Qwen3-4B")
219+
cfg = load_config(str(cfg_file))
220+
assert cfg.llm.model_id == "Qwen/Qwen3-4B"
221+
222+
223+
def test_env_override_api_base(tmp_path, monkeypatch):
224+
"""MAS_AVIARY_API_BASE overrides llm.api_base."""
225+
cfg_file = tmp_path / "test.yaml"
226+
cfg_file.write_text(yaml.dump({"llm": {"backend": "vllm"}}))
227+
monkeypatch.setenv("MAS_AVIARY_API_BASE", "http://gpu-box:8080/v1")
228+
cfg = load_config(str(cfg_file))
229+
assert cfg.llm.api_base == "http://gpu-box:8080/v1"
230+
231+
232+
def test_env_override_not_set(tmp_path):
233+
"""Without env vars, YAML values are preserved as-is."""
234+
cfg_file = tmp_path / "test.yaml"
235+
cfg_file.write_text(yaml.dump({
236+
"mcp": {
237+
"mode": "real",
238+
"servers": [{"url": "http://yaml-host:8600/mcp"}],
239+
}
240+
}))
241+
for var in ("MAS_AVIARY_MCP_URL", "MAS_AVIARY_MCP_MODE",
242+
"MAS_AVIARY_MODEL_ID", "MAS_AVIARY_API_BASE"):
243+
os.environ.pop(var, None)
244+
cfg = load_config(str(cfg_file))
245+
assert cfg.mcp.servers[0].url == "http://yaml-host:8600/mcp"

0 commit comments

Comments
 (0)