@@ -1539,6 +1539,214 @@ def test_load_mcp_servers(tmp_path: Path):
15391539 load_mcp_servers (tmp_path / 'does_not_exist.json' )
15401540
15411541
1542+ def test_load_mcp_servers_with_env_vars (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1543+ """Test environment variable expansion in config files."""
1544+ config = tmp_path / 'mcp.json'
1545+
1546+ # Test with environment variables in command
1547+ monkeypatch .setenv ('PYTHON_CMD' , 'python3' )
1548+ monkeypatch .setenv ('MCP_MODULE' , 'tests.mcp_server' )
1549+ config .write_text ('{"mcpServers": {"my_server": {"command": "${PYTHON_CMD}", "args": ["-m", "${MCP_MODULE}"]}}}' )
1550+
1551+ servers = load_mcp_servers (config )
1552+
1553+ assert len (servers ) == 1
1554+ server = servers [0 ]
1555+ assert isinstance (server , MCPServerStdio )
1556+ assert server .command == 'python3'
1557+ assert server .args == ['-m' , 'tests.mcp_server' ]
1558+ assert server .id == 'my_server'
1559+ assert server .tool_prefix == 'my_server'
1560+
1561+
1562+ def test_load_mcp_servers_env_var_in_env_dict (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1563+ """Test environment variable expansion in env dict."""
1564+ config = tmp_path / 'mcp.json'
1565+
1566+ # Test with environment variables in env dict
1567+ monkeypatch .setenv ('API_KEY' , 'secret123' )
1568+ config .write_text (
1569+ '{"mcpServers": {"my_server": {"command": "python", "args": ["-m", "tests.mcp_server"], '
1570+ '"env": {"API_KEY": "${API_KEY}"}}}}'
1571+ )
1572+
1573+ servers = load_mcp_servers (config )
1574+
1575+ assert len (servers ) == 1
1576+ server = servers [0 ]
1577+ assert isinstance (server , MCPServerStdio )
1578+ assert server .env == {'API_KEY' : 'secret123' }
1579+
1580+
1581+ def test_load_mcp_servers_env_var_expansion_url (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1582+ """Test environment variable expansion in URL."""
1583+ config = tmp_path / 'mcp.json'
1584+
1585+ # Test with environment variables in URL
1586+ monkeypatch .setenv ('SERVER_HOST' , 'example.com' )
1587+ monkeypatch .setenv ('SERVER_PORT' , '8080' )
1588+ config .write_text ('{"mcpServers": {"web_server": {"url": "https://${SERVER_HOST}:${SERVER_PORT}/mcp"}}}' )
1589+
1590+ servers = load_mcp_servers (config )
1591+
1592+ assert len (servers ) == 1
1593+ server = servers [0 ]
1594+ assert isinstance (server , MCPServerStreamableHTTP )
1595+ assert server .url == 'https://example.com:8080/mcp'
1596+
1597+
1598+ def test_load_mcp_servers_undefined_env_var (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1599+ """Test that undefined environment variables raise an error."""
1600+ config = tmp_path / 'mcp.json'
1601+
1602+ # Make sure the environment variable is not set
1603+ monkeypatch .delenv ('UNDEFINED_VAR' , raising = False )
1604+
1605+ config .write_text ('{"mcpServers": {"my_server": {"command": "${UNDEFINED_VAR}", "args": []}}}' )
1606+
1607+ with pytest .raises (ValueError , match = 'Environment variable \\ $\\ {UNDEFINED_VAR\\ } is not defined' ):
1608+ load_mcp_servers (config )
1609+
1610+
1611+ def test_load_mcp_servers_partial_env_vars (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1612+ """Test environment variables in partial strings."""
1613+ config = tmp_path / 'mcp.json'
1614+
1615+ monkeypatch .setenv ('HOST' , 'example.com' )
1616+ monkeypatch .setenv ('PATH_SUFFIX' , 'mcp' )
1617+ config .write_text ('{"mcpServers": {"server": {"url": "https://${HOST}/api/${PATH_SUFFIX}"}}}' )
1618+
1619+ servers = load_mcp_servers (config )
1620+
1621+ assert len (servers ) == 1
1622+ server = servers [0 ]
1623+ assert isinstance (server , MCPServerStreamableHTTP )
1624+ assert server .url == 'https://example.com/api/mcp'
1625+
1626+
1627+ def test_load_mcp_servers_with_non_string_values (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1628+ """Test that non-string primitive values (int, bool, null) in nested structures are passed through unchanged."""
1629+ config = tmp_path / 'mcp.json'
1630+
1631+ # Create a config with environment variables and extra fields containing primitives
1632+ # The extra fields will be ignored during validation but go through _expand_env_vars
1633+ monkeypatch .setenv ('PYTHON_CMD' , 'python' )
1634+ config .write_text (
1635+ '{"mcpServers": {"my_server": {"command": "${PYTHON_CMD}", "args": ["-m", "tests.mcp_server"], '
1636+ '"metadata": {"count": 42, "enabled": true, "value": null}}}}'
1637+ )
1638+
1639+ # This should successfully expand env vars and ignore the metadata field
1640+ servers = load_mcp_servers (config )
1641+
1642+ assert len (servers ) == 1
1643+ server = servers [0 ]
1644+ assert isinstance (server , MCPServerStdio )
1645+ assert server .command == 'python'
1646+
1647+
1648+ def test_load_mcp_servers_with_default_values (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1649+ """Test ${VAR:-default} syntax for environment variable expansion."""
1650+ config = tmp_path / 'mcp.json'
1651+
1652+ # Test with undefined variable using default
1653+ monkeypatch .delenv ('UNDEFINED_VAR' , raising = False )
1654+ config .write_text ('{"mcpServers": {"server": {"command": "${UNDEFINED_VAR:-python3}", "args": []}}}' )
1655+
1656+ servers = load_mcp_servers (config )
1657+ assert len (servers ) == 1
1658+ server = servers [0 ]
1659+ assert isinstance (server , MCPServerStdio )
1660+ assert server .command == 'python3'
1661+
1662+ # Test with defined variable (should use actual value, not default)
1663+ monkeypatch .setenv ('DEFINED_VAR' , 'actual_value' )
1664+ config .write_text ('{"mcpServers": {"server": {"command": "${DEFINED_VAR:-default_value}", "args": []}}}' )
1665+
1666+ servers = load_mcp_servers (config )
1667+ assert len (servers ) == 1
1668+ server = servers [0 ]
1669+ assert isinstance (server , MCPServerStdio )
1670+ assert server .command == 'actual_value'
1671+
1672+ # Test with empty string as default
1673+ monkeypatch .delenv ('UNDEFINED_VAR' , raising = False )
1674+ config .write_text ('{"mcpServers": {"server": {"command": "${UNDEFINED_VAR:-}", "args": []}}}' )
1675+
1676+ servers = load_mcp_servers (config )
1677+ assert len (servers ) == 1
1678+ server = servers [0 ]
1679+ assert isinstance (server , MCPServerStdio )
1680+ assert server .command == ''
1681+
1682+
1683+ def test_load_mcp_servers_with_default_values_in_url (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1684+ """Test ${VAR:-default} syntax in URLs."""
1685+ config = tmp_path / 'mcp.json'
1686+
1687+ # Test with default values in URL
1688+ monkeypatch .delenv ('HOST' , raising = False )
1689+ monkeypatch .setenv ('PROTOCOL' , 'https' )
1690+ config .write_text ('{"mcpServers": {"server": {"url": "${PROTOCOL:-http}://${HOST:-localhost}:${PORT:-8080}/mcp"}}}' )
1691+
1692+ servers = load_mcp_servers (config )
1693+ assert len (servers ) == 1
1694+ server = servers [0 ]
1695+ assert isinstance (server , MCPServerStreamableHTTP )
1696+ assert server .url == 'https://localhost:8080/mcp'
1697+
1698+
1699+ def test_load_mcp_servers_with_default_values_in_env_dict (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1700+ """Test ${VAR:-default} syntax in env dictionary."""
1701+ config = tmp_path / 'mcp.json'
1702+
1703+ monkeypatch .delenv ('API_KEY' , raising = False )
1704+ monkeypatch .setenv ('CUSTOM_VAR' , 'custom_value' )
1705+ config .write_text (
1706+ '{"mcpServers": {"server": {"command": "python", "args": [], '
1707+ '"env": {"API_KEY": "${API_KEY:-default_key}", "CUSTOM": "${CUSTOM_VAR:-fallback}"}}}}'
1708+ )
1709+
1710+ servers = load_mcp_servers (config )
1711+ assert len (servers ) == 1
1712+ server = servers [0 ]
1713+ assert isinstance (server , MCPServerStdio )
1714+ assert server .env == {'API_KEY' : 'default_key' , 'CUSTOM' : 'custom_value' }
1715+
1716+
1717+ def test_load_mcp_servers_with_complex_default_values (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1718+ """Test ${VAR:-default} syntax with special characters in default."""
1719+ config = tmp_path / 'mcp.json'
1720+
1721+ monkeypatch .delenv ('PATH_VAR' , raising = False )
1722+ # Test default with slashes, dots, and dashes
1723+ config .write_text ('{"mcpServers": {"server": {"command": "${PATH_VAR:-/usr/local/bin/python-3.10}", "args": []}}}' )
1724+
1725+ servers = load_mcp_servers (config )
1726+ assert len (servers ) == 1
1727+ server = servers [0 ]
1728+ assert isinstance (server , MCPServerStdio )
1729+ assert server .command == '/usr/local/bin/python-3.10'
1730+
1731+
1732+ def test_load_mcp_servers_with_mixed_syntax (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
1733+ """Test mixing ${VAR} and ${VAR:-default} syntax in the same config."""
1734+ config = tmp_path / 'mcp.json'
1735+
1736+ monkeypatch .setenv ('REQUIRED_VAR' , 'required_value' )
1737+ monkeypatch .delenv ('OPTIONAL_VAR' , raising = False )
1738+ config .write_text (
1739+ '{"mcpServers": {"server": {"command": "${REQUIRED_VAR}", "args": ["${OPTIONAL_VAR:-default_arg}"]}}}'
1740+ )
1741+
1742+ servers = load_mcp_servers (config )
1743+ assert len (servers ) == 1
1744+ server = servers [0 ]
1745+ assert isinstance (server , MCPServerStdio )
1746+ assert server .command == 'required_value'
1747+ assert server .args == ['default_arg' ]
1748+
1749+
15421750async def test_server_info (mcp_server : MCPServerStdio ) -> None :
15431751 with pytest .raises (
15441752 AttributeError , match = 'The `MCPServerStdio.server_info` is only instantiated after initialization.'
0 commit comments