From d96f0f0d8d9af482efbd90ef3568c1cf63a776d6 Mon Sep 17 00:00:00 2001 From: Walter Gillett Date: Sun, 9 Nov 2025 21:33:44 -0500 Subject: [PATCH 1/5] Make it possible to use environment variables in mcp.json --- docs/mcp/client.md | 26 ++++++++ pydantic_ai_slim/pydantic_ai/mcp.py | 42 ++++++++++++- tests/test_mcp.py | 96 +++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 1 deletion(-) diff --git a/docs/mcp/client.md b/docs/mcp/client.md index 59f96bf865..bb9fb128c5 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -187,6 +187,32 @@ The configuration file should be a JSON file with an `mcpServers` object contain We made this decision given that the SSE transport is deprecated. +### Environment Variables + +The configuration file supports environment variable expansion using the `${VAR_NAME}` syntax. This is useful for keeping sensitive information like API keys or host names out of your configuration files: + +```json {title="mcp_config_with_env.json"} +{ + "mcpServers": { + "python-runner": { + "command": "${PYTHON_CMD}", + "args": ["run", "${MCP_MODULE}", "stdio"], + "env": { + "API_KEY": "${MY_API_KEY}" + } + }, + "weather-api": { + "url": "https://${SERVER_HOST}:${SERVER_PORT}/sse" + } + } +} +``` + +When loading this configuration with [`load_mcp_servers()`][pydantic_ai.mcp.load_mcp_servers], the `${VAR_NAME}` references will be replaced with the corresponding environment variable values. + +!!! warning + If a referenced environment variable is not defined, a `ValueError` will be raised. Make sure all required environment variables are set before loading the configuration. + ### Usage ```python {title="mcp_config_loader.py" test="skip"} diff --git a/pydantic_ai_slim/pydantic_ai/mcp.py b/pydantic_ai_slim/pydantic_ai/mcp.py index 778a0f26f2..855d467f01 100644 --- a/pydantic_ai_slim/pydantic_ai/mcp.py +++ b/pydantic_ai_slim/pydantic_ai/mcp.py @@ -2,6 +2,8 @@ import base64 import functools +import os +import re import warnings from abc import ABC, abstractmethod from asyncio import Lock @@ -927,9 +929,44 @@ class MCPServerConfig(BaseModel): ] +def _expand_env_vars(value: Any) -> Any: + """Recursively expand environment variables in a JSON structure. + + Environment variables can be referenced using ${VAR_NAME} syntax. + + Args: + value: The value to expand (can be str, dict, list, or other JSON types). + + Returns: + The value with all environment variables expanded. + + Raises: + ValueError: If an environment variable is not defined. + """ + if isinstance(value, str): + # Find all environment variable references in the string + env_var_pattern = re.compile(r'\$\{([^}]+)\}') + matches = env_var_pattern.findall(value) + + for var_name in matches: + if var_name not in os.environ: + raise ValueError(f'Environment variable ${{{var_name}}} is not defined') + value = value.replace(f'${{{var_name}}}', os.environ[var_name]) + + return value + elif isinstance(value, dict): + return {k: _expand_env_vars(v) for k, v in value.items()} # type: ignore[misc] + elif isinstance(value, list): + return [_expand_env_vars(item) for item in value] # type: ignore[misc] + else: + return value + + def load_mcp_servers(config_path: str | Path) -> list[MCPServerStdio | MCPServerStreamableHTTP | MCPServerSSE]: """Load MCP servers from a configuration file. + Environment variables can be referenced in the configuration file using ${VAR_NAME} syntax. + Args: config_path: The path to the configuration file. @@ -939,13 +976,16 @@ def load_mcp_servers(config_path: str | Path) -> list[MCPServerStdio | MCPServer Raises: FileNotFoundError: If the configuration file does not exist. ValidationError: If the configuration file does not match the schema. + ValueError: If an environment variable referenced in the configuration is not defined. """ config_path = Path(config_path) if not config_path.exists(): raise FileNotFoundError(f'Config file {config_path} not found') - config = MCPServerConfig.model_validate_json(config_path.read_bytes()) + config_data = pydantic_core.from_json(config_path.read_bytes()) + expanded_config_data = _expand_env_vars(config_data) + config = MCPServerConfig.model_validate(expanded_config_data) servers: list[MCPServerStdio | MCPServerStreamableHTTP | MCPServerSSE] = [] for name, server in config.mcp_servers.items(): diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 2408b9b267..217f917419 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -1481,6 +1481,102 @@ def test_load_mcp_servers(tmp_path: Path): load_mcp_servers(tmp_path / 'does_not_exist.json') +def test_load_mcp_servers_with_env_vars(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test environment variable expansion in config files.""" + config = tmp_path / 'mcp.json' + + # Test with environment variables in command + monkeypatch.setenv('PYTHON_CMD', 'python3') + monkeypatch.setenv('MCP_MODULE', 'tests.mcp_server') + config.write_text('{"mcpServers": {"my_server": {"command": "${PYTHON_CMD}", "args": ["-m", "${MCP_MODULE}"]}}}') + + servers = load_mcp_servers(config) + + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.command == 'python3' + assert server.args == ['-m', 'tests.mcp_server'] + assert server.id == 'my_server' + assert server.tool_prefix == 'my_server' + + # Test with environment variables in URL + monkeypatch.setenv('SERVER_HOST', 'example.com') + monkeypatch.setenv('SERVER_PORT', '8080') + config.write_text('{"mcpServers": {"web_server": {"url": "https://${SERVER_HOST}:${SERVER_PORT}/mcp"}}}') + + servers = load_mcp_servers(config) + + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStreamableHTTP) + assert server.url == 'https://example.com:8080/mcp' + + # Test with environment variables in env dict + monkeypatch.setenv('API_KEY', 'secret123') + config.write_text( + '{"mcpServers": {"my_server": {"command": "python", "args": ["-m", "tests.mcp_server"], ' + '"env": {"API_KEY": "${API_KEY}"}}}}' + ) + + servers = load_mcp_servers(config) + + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.env == {'API_KEY': 'secret123'} + + +def test_load_mcp_servers_undefined_env_var(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test that undefined environment variables raise an error.""" + config = tmp_path / 'mcp.json' + + # Make sure the environment variable is not set + monkeypatch.delenv('UNDEFINED_VAR', raising=False) + + config.write_text('{"mcpServers": {"my_server": {"command": "${UNDEFINED_VAR}", "args": []}}}') + + with pytest.raises(ValueError, match='Environment variable \\$\\{UNDEFINED_VAR\\} is not defined'): + load_mcp_servers(config) + + +def test_load_mcp_servers_partial_env_vars(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test environment variables in partial strings.""" + config = tmp_path / 'mcp.json' + + monkeypatch.setenv('HOST', 'example.com') + monkeypatch.setenv('PATH_SUFFIX', 'mcp') + config.write_text('{"mcpServers": {"server": {"url": "https://${HOST}/api/${PATH_SUFFIX}"}}}') + + servers = load_mcp_servers(config) + + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStreamableHTTP) + assert server.url == 'https://example.com/api/mcp' + + +def test_load_mcp_servers_with_non_string_values(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test that non-string primitive values (int, bool, null) in nested structures are passed through unchanged.""" + config = tmp_path / 'mcp.json' + + # Create a config with environment variables and extra fields containing primitives + # The extra fields will be ignored during validation but go through _expand_env_vars + monkeypatch.setenv('PYTHON_CMD', 'python') + config.write_text( + '{"mcpServers": {"my_server": {"command": "${PYTHON_CMD}", "args": ["-m", "tests.mcp_server"], ' + '"metadata": {"count": 42, "enabled": true, "value": null}}}}' + ) + + # This should successfully expand env vars and ignore the metadata field + servers = load_mcp_servers(config) + + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.command == 'python' + + async def test_server_info(mcp_server: MCPServerStdio) -> None: with pytest.raises( AttributeError, match='The `MCPServerStdio.server_info` is only instantiated after initialization.' From 7e68fd0754f7eec0f961caeb2fb6a076b0e8a882 Mon Sep 17 00:00:00 2001 From: Walter Gillett Date: Mon, 10 Nov 2025 14:50:35 -0500 Subject: [PATCH 2/5] Handle code review feedback: add support for default syntax like Claude Code; compile a regex only once; use re.sub. --- docs/mcp/client.md | 15 ++-- pydantic_ai_slim/pydantic_ai/mcp.py | 40 +++++++--- tests/test_mcp.py | 117 ++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 15 deletions(-) diff --git a/docs/mcp/client.md b/docs/mcp/client.md index bb9fb128c5..71e15b77a2 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -189,29 +189,34 @@ The configuration file should be a JSON file with an `mcpServers` object contain ### Environment Variables -The configuration file supports environment variable expansion using the `${VAR_NAME}` syntax. This is useful for keeping sensitive information like API keys or host names out of your configuration files: +The configuration file supports environment variable expansion using the `${VAR}` and `${VAR:-default}` syntax, +[like Claude Code](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json). +This is useful for keeping sensitive information like API keys or host names out of your configuration files: ```json {title="mcp_config_with_env.json"} { "mcpServers": { "python-runner": { - "command": "${PYTHON_CMD}", + "command": "${PYTHON_CMD:-python3}", "args": ["run", "${MCP_MODULE}", "stdio"], "env": { "API_KEY": "${MY_API_KEY}" } }, "weather-api": { - "url": "https://${SERVER_HOST}:${SERVER_PORT}/sse" + "url": "https://${SERVER_HOST:-localhost}:${SERVER_PORT:-8080}/sse" } } } ``` -When loading this configuration with [`load_mcp_servers()`][pydantic_ai.mcp.load_mcp_servers], the `${VAR_NAME}` references will be replaced with the corresponding environment variable values. +When loading this configuration with [`load_mcp_servers()`][pydantic_ai.mcp.load_mcp_servers]: + +- `${VAR}` references will be replaced with the corresponding environment variable values. +- `${VAR:-default}` references will use the environment variable value if set, otherwise the default value. !!! warning - If a referenced environment variable is not defined, a `ValueError` will be raised. Make sure all required environment variables are set before loading the configuration. + If a referenced environment variable using `${VAR}` syntax is not defined, a `ValueError` will be raised. Use the `${VAR:-default}` syntax to provide a fallback value. ### Usage diff --git a/pydantic_ai_slim/pydantic_ai/mcp.py b/pydantic_ai_slim/pydantic_ai/mcp.py index 855d467f01..43c01c1912 100644 --- a/pydantic_ai_slim/pydantic_ai/mcp.py +++ b/pydantic_ai_slim/pydantic_ai/mcp.py @@ -53,6 +53,13 @@ ) ) +# Environment variable expansion pattern +# Supports both ${VAR_NAME} and ${VAR_NAME:-default} syntax +# Group 1: variable name +# Group 2: the ':-' separator (to detect if default syntax is used) +# Group 3: the default value (can be empty) +_ENV_VAR_PATTERN = re.compile(r'\$\{([^}:]+)(:-([^}]*))?\}') + class MCPServer(AbstractToolset[Any], ABC): """Base class for attaching agents to MCP servers. @@ -932,7 +939,8 @@ class MCPServerConfig(BaseModel): def _expand_env_vars(value: Any) -> Any: """Recursively expand environment variables in a JSON structure. - Environment variables can be referenced using ${VAR_NAME} syntax. + Environment variables can be referenced using ${VAR_NAME} syntax, + or ${VAR_NAME:-default} syntax to provide a default value if the variable is not set. Args: value: The value to expand (can be str, dict, list, or other JSON types). @@ -941,17 +949,27 @@ def _expand_env_vars(value: Any) -> Any: The value with all environment variables expanded. Raises: - ValueError: If an environment variable is not defined. + ValueError: If an environment variable is not defined and no default value is provided. """ if isinstance(value, str): # Find all environment variable references in the string - env_var_pattern = re.compile(r'\$\{([^}]+)\}') - matches = env_var_pattern.findall(value) - - for var_name in matches: - if var_name not in os.environ: + # Supports both ${VAR_NAME} and ${VAR_NAME:-default} syntax + def replace_match(match: re.Match[str]) -> str: + var_name = match.group(1) + has_default = match.group(2) is not None + default_value = match.group(3) if has_default else None + + # Check if variable exists in environment + if var_name in os.environ: + return os.environ[var_name] + elif has_default: + # Use default value if the :- syntax was present (even if empty string) + return default_value or '' + else: + # No default value and variable not set - raise error raise ValueError(f'Environment variable ${{{var_name}}} is not defined') - value = value.replace(f'${{{var_name}}}', os.environ[var_name]) + + value = _ENV_VAR_PATTERN.sub(replace_match, value) return value elif isinstance(value, dict): @@ -965,7 +983,9 @@ def _expand_env_vars(value: Any) -> Any: def load_mcp_servers(config_path: str | Path) -> list[MCPServerStdio | MCPServerStreamableHTTP | MCPServerSSE]: """Load MCP servers from a configuration file. - Environment variables can be referenced in the configuration file using ${VAR_NAME} syntax. + Environment variables can be referenced in the configuration file using: + - `${VAR_NAME}` syntax - expands to the value of VAR_NAME, raises error if not defined + - `${VAR_NAME:-default}` syntax - expands to VAR_NAME if set, otherwise uses the default value Args: config_path: The path to the configuration file. @@ -976,7 +996,7 @@ def load_mcp_servers(config_path: str | Path) -> list[MCPServerStdio | MCPServer Raises: FileNotFoundError: If the configuration file does not exist. ValidationError: If the configuration file does not match the schema. - ValueError: If an environment variable referenced in the configuration is not defined. + ValueError: If an environment variable referenced in the configuration is not defined and no default value is provided. """ config_path = Path(config_path) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 217f917419..a067189159 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -121,6 +121,21 @@ async def test_aexit_called_more_times_than_aenter(): await server.__aexit__(None, None, None) +async def test_is_running(): + """Test the is_running property.""" + server = MCPServerStdio('python', ['-m', 'tests.mcp_server']) + + # Server should not be running initially + assert not server.is_running + + # Server should be running inside the context manager + async with server: + assert server.is_running + + # Server should not be running after exiting the context manager + assert not server.is_running + + async def test_stdio_server_with_tool_prefix(run_context: RunContext[int]): server = MCPServerStdio('python', ['-m', 'tests.mcp_server'], tool_prefix='foo') async with server: @@ -1577,6 +1592,108 @@ def test_load_mcp_servers_with_non_string_values(tmp_path: Path, monkeypatch: py assert server.command == 'python' +def test_load_mcp_servers_with_default_values(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test ${VAR:-default} syntax for environment variable expansion.""" + config = tmp_path / 'mcp.json' + + # Test with undefined variable using default + monkeypatch.delenv('UNDEFINED_VAR', raising=False) + config.write_text('{"mcpServers": {"server": {"command": "${UNDEFINED_VAR:-python3}", "args": []}}}') + + servers = load_mcp_servers(config) + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.command == 'python3' + + # Test with defined variable (should use actual value, not default) + monkeypatch.setenv('DEFINED_VAR', 'actual_value') + config.write_text('{"mcpServers": {"server": {"command": "${DEFINED_VAR:-default_value}", "args": []}}}') + + servers = load_mcp_servers(config) + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.command == 'actual_value' + + # Test with empty string as default + monkeypatch.delenv('UNDEFINED_VAR', raising=False) + config.write_text('{"mcpServers": {"server": {"command": "${UNDEFINED_VAR:-}", "args": []}}}') + + servers = load_mcp_servers(config) + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.command == '' + + +def test_load_mcp_servers_with_default_values_in_url(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test ${VAR:-default} syntax in URLs.""" + config = tmp_path / 'mcp.json' + + # Test with default values in URL + monkeypatch.delenv('HOST', raising=False) + monkeypatch.setenv('PROTOCOL', 'https') + config.write_text('{"mcpServers": {"server": {"url": "${PROTOCOL:-http}://${HOST:-localhost}:${PORT:-8080}/mcp"}}}') + + servers = load_mcp_servers(config) + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStreamableHTTP) + assert server.url == 'https://localhost:8080/mcp' + + +def test_load_mcp_servers_with_default_values_in_env_dict(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test ${VAR:-default} syntax in env dictionary.""" + config = tmp_path / 'mcp.json' + + monkeypatch.delenv('API_KEY', raising=False) + monkeypatch.setenv('CUSTOM_VAR', 'custom_value') + config.write_text( + '{"mcpServers": {"server": {"command": "python", "args": [], ' + '"env": {"API_KEY": "${API_KEY:-default_key}", "CUSTOM": "${CUSTOM_VAR:-fallback}"}}}}' + ) + + servers = load_mcp_servers(config) + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.env == {'API_KEY': 'default_key', 'CUSTOM': 'custom_value'} + + +def test_load_mcp_servers_with_complex_default_values(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test ${VAR:-default} syntax with special characters in default.""" + config = tmp_path / 'mcp.json' + + monkeypatch.delenv('PATH_VAR', raising=False) + # Test default with slashes, dots, and dashes + config.write_text('{"mcpServers": {"server": {"command": "${PATH_VAR:-/usr/local/bin/python-3.10}", "args": []}}}') + + servers = load_mcp_servers(config) + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.command == '/usr/local/bin/python-3.10' + + +def test_load_mcp_servers_with_mixed_syntax(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test mixing ${VAR} and ${VAR:-default} syntax in the same config.""" + config = tmp_path / 'mcp.json' + + monkeypatch.setenv('REQUIRED_VAR', 'required_value') + monkeypatch.delenv('OPTIONAL_VAR', raising=False) + config.write_text( + '{"mcpServers": {"server": {"command": "${REQUIRED_VAR}", "args": ["${OPTIONAL_VAR:-default_arg}"]}}}' + ) + + servers = load_mcp_servers(config) + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStdio) + assert server.command == 'required_value' + assert server.args == ['default_arg'] + + async def test_server_info(mcp_server: MCPServerStdio) -> None: with pytest.raises( AttributeError, match='The `MCPServerStdio.server_info` is only instantiated after initialization.' From c15e1a8ef8c2983e4ef28d032d8ebc371e3cdf68 Mon Sep 17 00:00:00 2001 From: Walter Gillett Date: Wed, 12 Nov 2025 22:28:17 -0500 Subject: [PATCH 3/5] Split up one test into three per code review feedback --- tests/test_mcp.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index a067189159..0c086a90bc 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -1515,17 +1515,10 @@ def test_load_mcp_servers_with_env_vars(tmp_path: Path, monkeypatch: pytest.Monk assert server.id == 'my_server' assert server.tool_prefix == 'my_server' - # Test with environment variables in URL - monkeypatch.setenv('SERVER_HOST', 'example.com') - monkeypatch.setenv('SERVER_PORT', '8080') - config.write_text('{"mcpServers": {"web_server": {"url": "https://${SERVER_HOST}:${SERVER_PORT}/mcp"}}}') - servers = load_mcp_servers(config) - - assert len(servers) == 1 - server = servers[0] - assert isinstance(server, MCPServerStreamableHTTP) - assert server.url == 'https://example.com:8080/mcp' +def test_load_mcp_servers_env_var_in_env_dict(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test environment variable expansion in env dict.""" + config = tmp_path / 'mcp.json' # Test with environment variables in env dict monkeypatch.setenv('API_KEY', 'secret123') @@ -1542,6 +1535,23 @@ def test_load_mcp_servers_with_env_vars(tmp_path: Path, monkeypatch: pytest.Monk assert server.env == {'API_KEY': 'secret123'} +def test_load_mcp_servers_env_var_expansion_url(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Test environment variable expansion in URL.""" + config = tmp_path / 'mcp.json' + + # Test with environment variables in URL + monkeypatch.setenv('SERVER_HOST', 'example.com') + monkeypatch.setenv('SERVER_PORT', '8080') + config.write_text('{"mcpServers": {"web_server": {"url": "https://${SERVER_HOST}:${SERVER_PORT}/mcp"}}}') + + servers = load_mcp_servers(config) + + assert len(servers) == 1 + server = servers[0] + assert isinstance(server, MCPServerStreamableHTTP) + assert server.url == 'https://example.com:8080/mcp' + + def test_load_mcp_servers_undefined_env_var(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): """Test that undefined environment variables raise an error.""" config = tmp_path / 'mcp.json' From b4e1fe4184a6e2b893e48da92cb700363310d1c5 Mon Sep 17 00:00:00 2001 From: Walter Gillett Date: Thu, 13 Nov 2025 16:58:53 -0500 Subject: [PATCH 4/5] Drop test_is_running per code review feedback. --- tests/test_mcp.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 08db8e3546..9d0654f9df 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -121,21 +121,6 @@ async def test_aexit_called_more_times_than_aenter(): await server.__aexit__(None, None, None) -async def test_is_running(): - """Test the is_running property.""" - server = MCPServerStdio('python', ['-m', 'tests.mcp_server']) - - # Server should not be running initially - assert not server.is_running - - # Server should be running inside the context manager - async with server: - assert server.is_running - - # Server should not be running after exiting the context manager - assert not server.is_running - - async def test_stdio_server_with_tool_prefix(run_context: RunContext[int]): server = MCPServerStdio('python', ['-m', 'tests.mcp_server'], tool_prefix='foo') async with server: From 9f0cc354e380adf924ae1d1f8dc0636c3ac27c67 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 13 Nov 2025 17:45:48 -0600 Subject: [PATCH 5/5] Update pydantic_ai_slim/pydantic_ai/mcp.py --- pydantic_ai_slim/pydantic_ai/mcp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/mcp.py b/pydantic_ai_slim/pydantic_ai/mcp.py index 43c01c1912..3d2b607b3f 100644 --- a/pydantic_ai_slim/pydantic_ai/mcp.py +++ b/pydantic_ai_slim/pydantic_ai/mcp.py @@ -939,8 +939,8 @@ class MCPServerConfig(BaseModel): def _expand_env_vars(value: Any) -> Any: """Recursively expand environment variables in a JSON structure. - Environment variables can be referenced using ${VAR_NAME} syntax, - or ${VAR_NAME:-default} syntax to provide a default value if the variable is not set. + Environment variables can be referenced using `${VAR_NAME}` syntax, + or `${VAR_NAME:-default}` syntax to provide a default value if the variable is not set. Args: value: The value to expand (can be str, dict, list, or other JSON types).