From 5267f760eff6ac342f081711980be6a636db7695 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:50:35 +0000 Subject: [PATCH 1/8] Initial plan From 61b86cd705a865f29664b3c9caaa6bd4d3ced89d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:59:36 +0000 Subject: [PATCH 2/8] Add env variable support for MCP servers Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- backend/modules/config/config_manager.py | 1 + backend/modules/mcp_tools/client.py | 21 +- .../modules/mcp_tools/test_client_auth.py | 9 +- .../modules/mcp_tools/test_client_env.py | 189 ++++++++++++++++++ 4 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 backend/tests/modules/mcp_tools/test_client_env.py diff --git a/backend/modules/config/config_manager.py b/backend/modules/config/config_manager.py index 7f811e6..5e41f04 100644 --- a/backend/modules/config/config_manager.py +++ b/backend/modules/config/config_manager.py @@ -101,6 +101,7 @@ class MCPServerConfig(BaseModel): enabled: bool = True command: Optional[List[str]] = None # Command to run server (for stdio servers) cwd: Optional[str] = None # Working directory for command + env: Optional[Dict[str, str]] = None # Environment variables for stdio servers url: Optional[str] = None # URL for HTTP servers type: str = "stdio" # Server type: "stdio" or "http" (deprecated, use transport) transport: Optional[str] = None # Explicit transport: "stdio", "http", "sse" - takes priority over auto-detection diff --git a/backend/modules/mcp_tools/client.py b/backend/modules/mcp_tools/client.py index c25958c..08e1c27 100644 --- a/backend/modules/mcp_tools/client.py +++ b/backend/modules/mcp_tools/client.py @@ -140,7 +140,22 @@ async def _initialize_single_client(self, server_name: str, config: Dict[str, An if command: # Custom command specified cwd = config.get("cwd") + env = config.get("env") logger.info(f"Working directory specified: {cwd}") + + # Resolve environment variables in env dict + resolved_env = None + if env is not None: + resolved_env = {} + for key, value in env.items(): + try: + resolved_env[key] = resolve_env_var(value) + logger.debug(f"Resolved env var {key} for {server_name}") + except ValueError as e: + logger.error(f"Failed to resolve env var {key} for {server_name}: {e}") + return None # Skip this server if env var resolution fails + logger.info(f"Environment variables specified: {list(resolved_env.keys())}") + if cwd: # Convert relative path to absolute path from project root if not os.path.isabs(cwd): @@ -155,7 +170,7 @@ async def _initialize_single_client(self, server_name: str, config: Dict[str, An logger.info(f"✓ Working directory exists: {cwd}") logger.info(f"Creating STDIO client for {server_name} with command: {command} in cwd: {cwd}") from fastmcp.client.transports import StdioTransport - transport = StdioTransport(command=command[0], args=command[1:], cwd=cwd) + transport = StdioTransport(command=command[0], args=command[1:], cwd=cwd, env=resolved_env) client = Client(transport) logger.info(f"✓ Successfully created STDIO MCP client for {server_name} with custom command and cwd") return client @@ -164,7 +179,9 @@ async def _initialize_single_client(self, server_name: str, config: Dict[str, An return None else: logger.info(f"No cwd specified, creating STDIO client for {server_name} with command: {command}") - client = Client(command) + from fastmcp.client.transports import StdioTransport + transport = StdioTransport(command=command[0], args=command[1:], env=resolved_env) + client = Client(transport) logger.info(f"✓ Successfully created STDIO MCP client for {server_name} with custom command") return client else: diff --git a/backend/tests/modules/mcp_tools/test_client_auth.py b/backend/tests/modules/mcp_tools/test_client_auth.py index 625e789..04ac990 100644 --- a/backend/tests/modules/mcp_tools/test_client_auth.py +++ b/backend/tests/modules/mcp_tools/test_client_auth.py @@ -139,7 +139,8 @@ async def test_missing_env_var_raises_error(self, caplog): @pytest.mark.asyncio @patch('backend.modules.mcp_tools.client.Client') - async def test_stdio_client_ignores_token(self, mock_client_class): + @patch('fastmcp.client.transports.StdioTransport') + async def test_stdio_client_ignores_token(self, mock_transport_class, mock_client_class): """stdio clients should ignore auth_token (no auth mechanism).""" server_config = { "command": ["python", "server.py"], @@ -155,8 +156,10 @@ async def test_stdio_client_ignores_token(self, mock_client_class): await manager._initialize_single_client("test-server", server_config) - # For stdio, the Client is called with the command, not URL and auth - mock_client_class.assert_called_once_with(["python", "server.py"]) + # For stdio, the Client is called with StdioTransport, not URL and auth + # The auth_token should be ignored for stdio transports + assert mock_transport_class.called + assert mock_client_class.called @pytest.mark.asyncio async def test_malformed_env_var_pattern(self, caplog): diff --git a/backend/tests/modules/mcp_tools/test_client_env.py b/backend/tests/modules/mcp_tools/test_client_env.py new file mode 100644 index 0000000..f52137f --- /dev/null +++ b/backend/tests/modules/mcp_tools/test_client_env.py @@ -0,0 +1,189 @@ +import os +import pytest +from unittest.mock import Mock, patch, AsyncMock, MagicMock +from backend.modules.mcp_tools.client import MCPToolManager + + +class TestMCPClientEnvironmentVariables: + """Test MCP client initialization with environment variables.""" + + @pytest.mark.asyncio + @patch('backend.modules.mcp_tools.client.Client') + @patch('fastmcp.client.transports.StdioTransport') + async def test_stdio_client_with_env_vars(self, mock_transport_class, mock_client_class, monkeypatch): + """Should pass environment variables to StdioTransport.""" + # Set up environment variables for resolution + monkeypatch.setenv("MY_ENV_VAR", "resolved-value") + + server_config = { + "command": ["python", "server.py"], + "cwd": "backend", + "env": { + "VAR1": "literal-value", + "VAR2": "another-literal" + } + } + + with patch('backend.modules.mcp_tools.client.config_manager') as mock_config_manager: + mock_config_manager.mcp_config.servers = {"test-server": Mock()} + mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config + + # Mock os.path.exists to return True for cwd + with patch('os.path.exists', return_value=True): + manager = MCPToolManager() + manager.servers_config = {"test-server": server_config} + + await manager._initialize_single_client("test-server", server_config) + + # Verify StdioTransport was called with env dict + assert mock_transport_class.called + call_kwargs = mock_transport_class.call_args[1] + assert "env" in call_kwargs + assert call_kwargs["env"] == { + "VAR1": "literal-value", + "VAR2": "another-literal" + } + + @pytest.mark.asyncio + @patch('backend.modules.mcp_tools.client.Client') + @patch('fastmcp.client.transports.StdioTransport') + async def test_stdio_client_with_env_var_resolution(self, mock_transport_class, mock_client_class, monkeypatch): + """Should resolve ${ENV_VAR} patterns in env values.""" + # Set up environment variables + monkeypatch.setenv("CLOUD_PROFILE", "my-profile-9") + monkeypatch.setenv("CLOUD_REGION", "us-east-7") + + server_config = { + "command": ["python", "server.py"], + "cwd": "backend", + "env": { + "PROFILE": "${CLOUD_PROFILE}", + "REGION": "${CLOUD_REGION}", + "LITERAL": "not-a-var" + } + } + + with patch('backend.modules.mcp_tools.client.config_manager') as mock_config_manager: + mock_config_manager.mcp_config.servers = {"test-server": Mock()} + mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config + + # Mock os.path.exists to return True for cwd + with patch('os.path.exists', return_value=True): + manager = MCPToolManager() + manager.servers_config = {"test-server": server_config} + + await manager._initialize_single_client("test-server", server_config) + + # Verify env vars were resolved + assert mock_transport_class.called + call_kwargs = mock_transport_class.call_args[1] + assert call_kwargs["env"] == { + "PROFILE": "my-profile-9", + "REGION": "us-east-7", + "LITERAL": "not-a-var" + } + + @pytest.mark.asyncio + @patch('backend.modules.mcp_tools.client.Client') + @patch('fastmcp.client.transports.StdioTransport') + async def test_stdio_client_without_env(self, mock_transport_class, mock_client_class): + """Should pass None when no env specified.""" + server_config = { + "command": ["python", "server.py"], + "cwd": "backend" + } + + with patch('backend.modules.mcp_tools.client.config_manager') as mock_config_manager: + mock_config_manager.mcp_config.servers = {"test-server": Mock()} + mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config + + # Mock os.path.exists to return True for cwd + with patch('os.path.exists', return_value=True): + manager = MCPToolManager() + manager.servers_config = {"test-server": server_config} + + await manager._initialize_single_client("test-server", server_config) + + # Verify env is None + assert mock_transport_class.called + call_kwargs = mock_transport_class.call_args[1] + assert call_kwargs["env"] is None + + @pytest.mark.asyncio + async def test_stdio_client_missing_env_var_fails(self, caplog): + """Should fail when env var resolution fails.""" + server_config = { + "command": ["python", "server.py"], + "cwd": "backend", + "env": { + "PROFILE": "${MISSING_VAR}" + } + } + + with patch('backend.modules.mcp_tools.client.config_manager') as mock_config_manager: + mock_config_manager.mcp_config.servers = {"test-server": Mock()} + mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config + + # Mock os.path.exists to return True for cwd + with patch('os.path.exists', return_value=True): + manager = MCPToolManager() + manager.servers_config = {"test-server": server_config} + + result = await manager._initialize_single_client("test-server", server_config) + + # Should return None and log error + assert result is None + assert "Failed to resolve env var" in caplog.text + assert "MISSING_VAR" in caplog.text + + @pytest.mark.asyncio + @patch('backend.modules.mcp_tools.client.Client') + @patch('fastmcp.client.transports.StdioTransport') + async def test_stdio_client_with_env_no_cwd(self, mock_transport_class, mock_client_class, monkeypatch): + """Should pass env vars even when no cwd specified.""" + monkeypatch.setenv("MY_VAR", "my-value") + + server_config = { + "command": ["python", "server.py"], + "env": { + "TEST_VAR": "${MY_VAR}" + } + } + + with patch('backend.modules.mcp_tools.client.config_manager') as mock_config_manager: + mock_config_manager.mcp_config.servers = {"test-server": Mock()} + mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config + + manager = MCPToolManager() + manager.servers_config = {"test-server": server_config} + + await manager._initialize_single_client("test-server", server_config) + + # Verify env was passed + assert mock_transport_class.called + call_kwargs = mock_transport_class.call_args[1] + assert call_kwargs["env"] == {"TEST_VAR": "my-value"} + + @pytest.mark.asyncio + @patch('backend.modules.mcp_tools.client.Client') + @patch('fastmcp.client.transports.StdioTransport') + async def test_stdio_client_empty_env_dict(self, mock_transport_class, mock_client_class): + """Should handle empty env dict.""" + server_config = { + "command": ["python", "server.py"], + "env": {} + } + + with patch('backend.modules.mcp_tools.client.config_manager') as mock_config_manager: + mock_config_manager.mcp_config.servers = {"test-server": Mock()} + mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config + + manager = MCPToolManager() + manager.servers_config = {"test-server": server_config} + + await manager._initialize_single_client("test-server", server_config) + + # Empty dict should become empty dict (not None) + assert mock_transport_class.called + call_kwargs = mock_transport_class.call_args[1] + assert call_kwargs["env"] == {} From 8ce2a6bf786563733fa2fc971bbfdebad9b0c253 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:03:03 +0000 Subject: [PATCH 3/8] Add documentation for env variable support Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- docs/02_admin_guide.md | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/02_admin_guide.md b/docs/02_admin_guide.md index ca1c202..a9f1c10 100644 --- a/docs/02_admin_guide.md +++ b/docs/02_admin_guide.md @@ -139,6 +139,11 @@ Here is an example of a server configuration that uses all available options. "groups": ["admin", "engineering"], "command": ["python", "mcp/MyExampleServer/main.py"], "cwd": "backend", + "env": { + "API_KEY": "${MY_API_KEY}", + "DEBUG_MODE": "false", + "MAX_RETRIES": "3" + }, "url": null, "transport": "stdio", "compliance_level": "Internal", @@ -158,6 +163,7 @@ Here is an example of a server configuration that uses all available options. * **`groups`**: (list of strings) A list of user groups that are allowed to access this server. If a user is not in any of these groups, the server will be hidden from them. * **`command`**: (list of strings) For servers using `stdio` transport, this is the command and its arguments used to start the server process. * **`cwd`**: (string) The working directory from which to run the `command`. +* **`env`**: (object) Environment variables to set for `stdio` servers. Keys are variable names, values can be literal strings or use environment variable substitution (e.g., `"${ENV_VAR}"`). This is only applicable to stdio servers and will be ignored for HTTP/SSE servers. * **`url`**: (string) For servers using `http` or `sse` transport, this is the URL of the server's endpoint. * **`transport`**: (string) The communication protocol to use. Can be `stdio`, `http`, or `sse`. This takes priority over auto-detection. * **`auth_token`**: (string) For HTTP/SSE servers, the bearer token used for authentication. Use environment variable substitution (e.g., `"${MCP_SERVER_TOKEN}"`) to avoid storing secrets in config files. Stdio servers ignore this field. @@ -209,6 +215,51 @@ export MCP_EXTERNAL_API_TOKEN="your-secret-api-key" - **Alternative**: For development/testing, you can use direct string values (not recommended for production) - **Never**: Commit tokens to `config/defaults/mcp.json` or any version-controlled files +### Environment Variables for Stdio Servers + +For stdio servers, you can pass custom environment variables to the server process using the `env` field. This is useful for: +- Configuring server behavior without modifying command arguments +- Passing credentials or API keys securely +- Setting runtime configuration options + +#### Example Configuration + +```json +{ + "my-external-tool": { + "command": ["wrapper-cli", "my.external.tool@latest", "--allow-write"], + "cwd": "backend", + "env": { + "CLOUD_PROFILE": "my-profile-9", + "CLOUD_REGION": "us-east-7", + "API_KEY": "${MY_API_KEY}", + "DEBUG_MODE": "false" + }, + "groups": ["users"] + } +} +``` + +Then set the environment variable before starting Atlas UI: +```bash +export MY_API_KEY="your-secret-api-key" +``` + +#### Environment Variable Features + +- **Literal Values**: Environment variables can contain literal string values (e.g., `"CLOUD_REGION": "us-east-7"`) +- **Variable Substitution**: Use `${VAR_NAME}` syntax to reference system environment variables (e.g., `"API_KEY": "${MY_API_KEY}"`) +- **Empty Values**: An empty object `{}` is valid and will set no environment variables +- **Error Handling**: If a referenced environment variable (e.g., `${MY_API_KEY}`) is not set in the system, the server initialization will fail with a clear error message +- **Stdio Only**: The `env` field only applies to stdio servers; it is ignored for HTTP/SSE servers + +#### Security Best Practices + +- Use environment variable substitution for all sensitive values (API keys, passwords, tokens) +- Never store secrets directly in the `env` object values +- Set environment variables via your deployment system (Docker, Kubernetes, systemd, etc.) +- Use different values for development, staging, and production environments + ### Access Control with Groups You can restrict access to MCP servers based on user groups. This is a critical feature for controlling which users can access powerful or sensitive tools. If a user is not in the required group, the server will be completely invisible to them in the UI, and any attempt to call its functions will be blocked. From 157de27ca8ce21ceb09b88b3655641446dad8641 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:19:15 +0000 Subject: [PATCH 4/8] Add env-demo MCP server to demonstrate environment variable support Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- backend/mcp/env-demo/README.md | 158 ++++++++++++++++++++ backend/mcp/env-demo/main.py | 199 ++++++++++++++++++++++++++ backend/tests/test_env_demo_server.py | 114 +++++++++++++++ config/defaults/mcp.json | 22 +++ config/overrides/mcp.json | 22 +++ 5 files changed, 515 insertions(+) create mode 100644 backend/mcp/env-demo/README.md create mode 100644 backend/mcp/env-demo/main.py create mode 100644 backend/tests/test_env_demo_server.py diff --git a/backend/mcp/env-demo/README.md b/backend/mcp/env-demo/README.md new file mode 100644 index 0000000..65ecc9e --- /dev/null +++ b/backend/mcp/env-demo/README.md @@ -0,0 +1,158 @@ +# Environment Variable Demo MCP Server + +This MCP server demonstrates the environment variable passing capability added to Atlas UI 3. It shows how to configure and use environment variables for MCP servers through the `mcp.json` configuration file. + +## Purpose + +This server provides tools to: +- Retrieve specific environment variables +- List all configured environment variables +- Demonstrate practical usage patterns for environment variables + +## Configuration + +The server is configured in `config/overrides/mcp.json` (or `config/defaults/mcp.json`) with the `env` field: + +```json +{ + "env-demo": { + "command": ["python", "mcp/env-demo/main.py"], + "cwd": "backend", + "env": { + "CLOUD_PROFILE": "demo-profile", + "CLOUD_REGION": "us-west-2", + "DEBUG_MODE": "true", + "ENVIRONMENT": "development", + "API_KEY": "${DEMO_API_KEY}" + }, + "groups": ["users"], + "description": "Demonstrates environment variable passing to MCP servers", + "compliance_level": "Public" + } +} +``` + +## Environment Variable Features + +### Literal Values +Set environment variables with literal string values: +```json +"env": { + "CLOUD_REGION": "us-west-2", + "DEBUG_MODE": "true" +} +``` + +### Variable Substitution +Reference system environment variables using `${VAR_NAME}` syntax: +```json +"env": { + "API_KEY": "${DEMO_API_KEY}" +} +``` + +Before starting Atlas UI, set the system environment variable: +```bash +export DEMO_API_KEY="your-secret-key" +``` + +## Available Tools + +### 1. `get_env_var` +Retrieves the value of a specific environment variable. + +**Input:** +- `var_name` (string): Name of the environment variable + +**Example Usage:** +``` +Get the value of CLOUD_REGION +``` + +### 2. `list_configured_env_vars` +Lists all configured environment variables that are commonly expected. + +**Example Usage:** +``` +Show me the configured environment variables +``` + +### 3. `demonstrate_env_usage` +Shows practical examples of using environment variables. + +**Input:** +- `operation` (string): Type of demonstration + - `"info"`: General information about env var configuration + - `"config"`: Demonstrates configuration usage (profile, region) + - `"credentials"`: Demonstrates secure credential handling + +**Example Usage:** +``` +Demonstrate how to use environment variables for configuration +``` + +## Use Cases + +### Cloud Configuration +```json +"env": { + "CLOUD_PROFILE": "production-profile", + "CLOUD_REGION": "us-east-1", + "AVAILABILITY_ZONE": "us-east-1a" +} +``` + +### API Credentials +```json +"env": { + "API_KEY": "${MY_SERVICE_API_KEY}", + "API_ENDPOINT": "https://api.example.com" +} +``` + +### Feature Flags +```json +"env": { + "DEBUG_MODE": "false", + "ENABLE_CACHING": "true", + "MAX_RETRIES": "3" +} +``` + +## Security Best Practices + +1. **Never commit secrets**: Use `${VAR_NAME}` substitution for sensitive values +2. **Set system env vars**: Configure sensitive values at the system level +3. **Use appropriate compliance levels**: Mark servers with sensitive access appropriately +4. **Document required variables**: Clearly document which env vars are needed + +## Testing + +To test this server: + +1. Set any optional environment variables: +```bash +export DEMO_API_KEY="test-key-123" +``` + +2. Start Atlas UI (the server will automatically load with the configured env vars) + +3. In the chat interface, try: +``` +List all configured environment variables for the env-demo server +``` + +``` +Get the value of CLOUD_REGION +``` + +``` +Demonstrate how environment variables are used for credentials +``` + +## Notes + +- Environment variables are only passed to stdio servers (not HTTP/SSE servers) +- If a `${VAR_NAME}` reference cannot be resolved, server initialization will fail with a clear error message +- Empty `env: {}` is valid and will set no environment variables +- The `env` field is optional; servers work without it as before diff --git a/backend/mcp/env-demo/main.py b/backend/mcp/env-demo/main.py new file mode 100644 index 0000000..308aa64 --- /dev/null +++ b/backend/mcp/env-demo/main.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +Environment Variable Demo MCP Server using FastMCP + +This server demonstrates the environment variable passing capability. +It reads environment variables that are configured in mcp.json and +exposes them through MCP tools. +""" + +import os +import time +from typing import Any, Dict + +from fastmcp import FastMCP + +# Initialize the MCP server +mcp = FastMCP("Environment Variable Demo") + + +@mcp.tool +def get_env_var(var_name: str) -> Dict[str, Any]: + """Get the value of a specific environment variable. + + This tool demonstrates how environment variables configured in mcp.json + are passed to the MCP server process. + + Args: + var_name: Name of the environment variable to retrieve + + Returns: + MCP contract shape with the environment variable value: + { + "results": { + "var_name": str, + "var_value": str or None, + "is_set": bool + }, + "meta_data": { + "elapsed_ms": float + } + } + """ + start = time.perf_counter() + + var_value = os.environ.get(var_name) + is_set = var_name in os.environ + + elapsed_ms = round((time.perf_counter() - start) * 1000, 3) + + return { + "results": { + "var_name": var_name, + "var_value": var_value, + "is_set": is_set + }, + "meta_data": { + "elapsed_ms": elapsed_ms + } + } + + +@mcp.tool +def list_configured_env_vars() -> Dict[str, Any]: + """List all environment variables that were configured in mcp.json. + + This tool shows which environment variables from the mcp.json configuration + are available to this server. It returns commonly expected configuration + variables that might be set. + + Returns: + MCP contract shape with environment variables: + { + "results": { + "configured_vars": dict of var_name -> var_value, + "total_count": int + }, + "meta_data": { + "elapsed_ms": float + } + } + """ + start = time.perf_counter() + + # List of common configuration environment variables + # This demonstrates what might be passed from mcp.json + common_config_vars = [ + "CLOUD_PROFILE", + "CLOUD_REGION", + "API_KEY", + "DEBUG_MODE", + "MAX_RETRIES", + "TIMEOUT_SECONDS", + "ENVIRONMENT", + "SERVICE_URL" + ] + + configured_vars = {} + for var_name in common_config_vars: + if var_name in os.environ: + configured_vars[var_name] = os.environ[var_name] + + elapsed_ms = round((time.perf_counter() - start) * 1000, 3) + + return { + "results": { + "configured_vars": configured_vars, + "total_count": len(configured_vars) + }, + "meta_data": { + "elapsed_ms": elapsed_ms + } + } + + +@mcp.tool +def demonstrate_env_usage(operation: str = "info") -> Dict[str, Any]: + """Demonstrate how environment variables can be used in MCP server operations. + + This tool shows practical examples of using environment variables for: + - Configuration (e.g., region, profile) + - Feature flags (e.g., debug mode) + - API credentials (e.g., API keys) + + Args: + operation: Type of demonstration ("info", "config", "credentials") + + Returns: + MCP contract shape with demonstration results: + { + "results": { + "operation": str, + "example": str, + "details": dict + }, + "meta_data": { + "elapsed_ms": float + } + } + """ + start = time.perf_counter() + + if operation == "config": + # Demonstrate configuration from environment + cloud_profile = os.environ.get("CLOUD_PROFILE", "default") + cloud_region = os.environ.get("CLOUD_REGION", "us-east-1") + + example = f"Using cloud profile '{cloud_profile}' in region '{cloud_region}'" + details = { + "profile": cloud_profile, + "region": cloud_region, + "source": "environment variables from mcp.json" + } + + elif operation == "credentials": + # Demonstrate secure credential handling + api_key = os.environ.get("API_KEY") + has_key = api_key is not None + + example = f"API key is {'configured' if has_key else 'not configured'}" + details = { + "has_api_key": has_key, + "key_length": len(api_key) if api_key else 0, + "masked_key": f"{api_key[:4]}...{api_key[-4:]}" if api_key and len(api_key) > 8 else None, + "source": "environment variable ${API_KEY} from mcp.json" + } + + else: # info + example = "Environment variables can be configured in mcp.json" + details = { + "usage": "Set env dict in mcp.json server configuration", + "syntax": { + "literal": "KEY: 'literal-value'", + "substitution": "KEY: '${SYSTEM_ENV_VAR}'" + }, + "example_config": { + "env": { + "CLOUD_PROFILE": "my-profile-9", + "CLOUD_REGION": "us-east-7", + "API_KEY": "${MY_API_KEY}" + } + } + } + + elapsed_ms = round((time.perf_counter() - start) * 1000, 3) + + return { + "results": { + "operation": operation, + "example": example, + "details": details + }, + "meta_data": { + "elapsed_ms": elapsed_ms + } + } + + +if __name__ == "__main__": + mcp.run() diff --git a/backend/tests/test_env_demo_server.py b/backend/tests/test_env_demo_server.py new file mode 100644 index 0000000..f43df99 --- /dev/null +++ b/backend/tests/test_env_demo_server.py @@ -0,0 +1,114 @@ +""" +Unit tests for the env-demo MCP server. +Tests the environment variable demonstration functionality. +""" +import os +import pytest +from unittest.mock import patch + +# Test that the server can be imported +def test_server_imports(): + """Test that the env-demo server module can be imported.""" + try: + import sys + sys.path.insert(0, '/home/runner/work/atlas-ui-3/atlas-ui-3/backend') + # Import the module by file path since directory has hyphen + import importlib.util + spec = importlib.util.spec_from_file_location( + "env_demo_main", + "/home/runner/work/atlas-ui-3/atlas-ui-3/backend/mcp/env-demo/main.py" + ) + module = importlib.util.module_from_spec(spec) + # Don't execute - just verify it can be loaded + assert spec is not None + assert module is not None + except Exception as e: + pytest.fail(f"Failed to import env-demo server: {e}") + + +def test_env_var_configuration(): + """Test that environment variables are accessible.""" + # Set test environment variables + os.environ["TEST_CLOUD_PROFILE"] = "test-profile" + os.environ["TEST_CLOUD_REGION"] = "test-region" + + # Verify they can be read + assert os.environ.get("TEST_CLOUD_PROFILE") == "test-profile" + assert os.environ.get("TEST_CLOUD_REGION") == "test-region" + + # Clean up + del os.environ["TEST_CLOUD_PROFILE"] + del os.environ["TEST_CLOUD_REGION"] + + +def test_env_var_substitution_pattern(): + """Test the ${VAR} pattern that should be resolved by config_manager.""" + # This tests the pattern that config_manager.resolve_env_var handles + # We test the pattern matching logic directly + import re + + # Set a test variable + os.environ["TEST_API_KEY"] = "secret-123" + + # Test the ${VAR} pattern matching (same as config_manager.resolve_env_var) + pattern = r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}' + + # Test resolution + test_value = "${TEST_API_KEY}" + match = re.match(pattern, test_value) + assert match is not None + var_name = match.group(1) + assert var_name == "TEST_API_KEY" + resolved = os.environ.get(var_name) + assert resolved == "secret-123" + + # Test literal value (no substitution) + test_value = "literal-value" + match = re.match(pattern, test_value) + assert match is None # Should not match + + # Test missing variable + test_value = "${MISSING_VAR}" + match = re.match(pattern, test_value) + assert match is not None + var_name = match.group(1) + assert var_name == "MISSING_VAR" + missing_var = os.environ.get(var_name) + assert missing_var is None # Variable doesn't exist + + # Clean up + del os.environ["TEST_API_KEY"] + + +def test_mcp_json_configuration(): + """Test that the env-demo server is properly configured in mcp.json.""" + import json + + # Check in overrides + overrides_path = "/home/runner/work/atlas-ui-3/atlas-ui-3/config/overrides/mcp.json" + with open(overrides_path) as f: + config = json.load(f) + + assert "env-demo" in config + server_config = config["env-demo"] + + # Verify required fields + assert "command" in server_config + assert "cwd" in server_config + assert "env" in server_config + assert "groups" in server_config + + # Verify env configuration + env_config = server_config["env"] + assert isinstance(env_config, dict) + assert "CLOUD_PROFILE" in env_config + assert "CLOUD_REGION" in env_config + assert "API_KEY" in env_config + + # Verify the API_KEY uses substitution pattern + assert env_config["API_KEY"].startswith("${") + assert env_config["API_KEY"].endswith("}") + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/config/defaults/mcp.json b/config/defaults/mcp.json index 5800575..235ea37 100644 --- a/config/defaults/mcp.json +++ b/config/defaults/mcp.json @@ -89,6 +89,28 @@ "help_email": "support@chatui.example.com", "compliance_level": "Public" }, + "env-demo": { + "command": [ + "python", + "mcp/env-demo/main.py" + ], + "cwd": "backend", + "env": { + "CLOUD_PROFILE": "demo-profile", + "CLOUD_REGION": "us-west-2", + "DEBUG_MODE": "true", + "ENVIRONMENT": "development", + "API_KEY": "${DEMO_API_KEY}" + }, + "groups": [ + "users" + ], + "description": "Demonstrates environment variable passing to MCP servers. Shows how to configure servers with env vars in mcp.json using both literal values and ${VAR} substitution.", + "author": "Chat UI Team", + "short_description": "Environment variable demonstration", + "help_email": "support@chatui.example.com", + "compliance_level": "Public" + }, "external-api-example": { "url": "http://127.0.0.1:8005/mcp", "transport": "http", diff --git a/config/overrides/mcp.json b/config/overrides/mcp.json index 10581bc..be86aad 100644 --- a/config/overrides/mcp.json +++ b/config/overrides/mcp.json @@ -97,6 +97,28 @@ ], "compliance_level": "Public" }, + "env-demo": { + "command": [ + "python", + "mcp/env-demo/main.py" + ], + "cwd": "backend", + "env": { + "CLOUD_PROFILE": "demo-profile", + "CLOUD_REGION": "us-west-2", + "DEBUG_MODE": "true", + "ENVIRONMENT": "development", + "API_KEY": "${DEMO_API_KEY}" + }, + "groups": [ + "users" + ], + "description": "Demonstrates environment variable passing to MCP servers. Shows how to configure servers with env vars in mcp.json using both literal values and ${VAR} substitution.", + "author": "Chat UI Team", + "short_description": "Environment variable demonstration", + "help_email": "support@chatui.example.com", + "compliance_level": "Public" + }, "external-api-example": { "enabled": true, "url": "http://127.0.0.1:8005/mcp", From a393939e18381c44e3aba7d062480eb3a553fddc Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 17 Nov 2025 22:49:02 +0000 Subject: [PATCH 5/8] feat(config): add env-demo MCP server configuration - Introduced a new configuration file in `config/overrides/env-var-mcp.json` for demonstrating environment variable passing in MCP servers. - The `env-demo` setup includes command execution, environment variables like CLOUD_PROFILE and API_KEY (with variable substitution), and metadata such as groups, descriptions, and compliance level. - This change provides a practical example for users to understand how to configure MCP servers using both literal values and `${VAR}` substitutions. --- config/overrides/env-var-mcp.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 config/overrides/env-var-mcp.json diff --git a/config/overrides/env-var-mcp.json b/config/overrides/env-var-mcp.json new file mode 100644 index 0000000..c822644 --- /dev/null +++ b/config/overrides/env-var-mcp.json @@ -0,0 +1,23 @@ + {"env-demo": { + "command": [ + "python", + "mcp/env-demo/main.py" + ], + "cwd": "backend", + "env": { + "CLOUD_PROFILE": "demo-profile", + "CLOUD_REGION": "us-west-2", + "DEBUG_MODE": "true", + "ENVIRONMENT": "development", + "API_KEY": "${DEMO_API_KEY}" + }, + "groups": [ + "users" + ], + "description": "Demonstrates environment variable passing to MCP servers. Shows how to configure servers with env vars in mcp.json using both literal values and ${VAR} substitution.", + "author": "Chat UI Team", + "short_description": "Environment variable demonstration", + "help_email": "support@chatui.example.com", + "compliance_level": "Public" + } +} \ No newline at end of file From b4129ffc4c9c4a370d7ab4da56ac64837623b609 Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 17 Nov 2025 22:51:15 +0000 Subject: [PATCH 6/8] refactor(tests): remove unused imports in test_client_env.py --- backend/tests/modules/mcp_tools/test_client_env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/modules/mcp_tools/test_client_env.py b/backend/tests/modules/mcp_tools/test_client_env.py index f52137f..43d19c7 100644 --- a/backend/tests/modules/mcp_tools/test_client_env.py +++ b/backend/tests/modules/mcp_tools/test_client_env.py @@ -1,6 +1,6 @@ import os import pytest -from unittest.mock import Mock, patch, AsyncMock, MagicMock +from unittest.mock import Mock, patch from backend.modules.mcp_tools.client import MCPToolManager From 606323daaca10be721c051af65298bfe0aa8e3b9 Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 17 Nov 2025 23:04:23 +0000 Subject: [PATCH 7/8] refactor: remove unused imports in test files Remove `import os` from test_client_env.py and `from unittest.mock import patch` from test_env_demo_server.py, as they were not utilized in the files, to clean up the codebase and reduce linting warnings. --- backend/tests/modules/mcp_tools/test_client_env.py | 2 +- backend/tests/test_env_demo_server.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/tests/modules/mcp_tools/test_client_env.py b/backend/tests/modules/mcp_tools/test_client_env.py index 43d19c7..117f2b4 100644 --- a/backend/tests/modules/mcp_tools/test_client_env.py +++ b/backend/tests/modules/mcp_tools/test_client_env.py @@ -1,4 +1,4 @@ -import os + import pytest from unittest.mock import Mock, patch from backend.modules.mcp_tools.client import MCPToolManager diff --git a/backend/tests/test_env_demo_server.py b/backend/tests/test_env_demo_server.py index f43df99..1aa1899 100644 --- a/backend/tests/test_env_demo_server.py +++ b/backend/tests/test_env_demo_server.py @@ -4,7 +4,6 @@ """ import os import pytest -from unittest.mock import patch # Test that the server can be imported def test_server_imports(): From 6aca4df79de6a030b936cae525fbf06b59db71ca Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 17 Nov 2025 23:04:48 +0000 Subject: [PATCH 8/8] refactor(test): Remove MCP JSON configuration test from env demo server Remove the test_mcp_json_configuration function that verified env-demo server setup in overrides/mcp.json, as this configuration check is no longer needed in the current test suite structure. --- backend/tests/test_env_demo_server.py | 29 +-------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/backend/tests/test_env_demo_server.py b/backend/tests/test_env_demo_server.py index 1aa1899..d36df83 100644 --- a/backend/tests/test_env_demo_server.py +++ b/backend/tests/test_env_demo_server.py @@ -79,34 +79,7 @@ def test_env_var_substitution_pattern(): del os.environ["TEST_API_KEY"] -def test_mcp_json_configuration(): - """Test that the env-demo server is properly configured in mcp.json.""" - import json - - # Check in overrides - overrides_path = "/home/runner/work/atlas-ui-3/atlas-ui-3/config/overrides/mcp.json" - with open(overrides_path) as f: - config = json.load(f) - - assert "env-demo" in config - server_config = config["env-demo"] - - # Verify required fields - assert "command" in server_config - assert "cwd" in server_config - assert "env" in server_config - assert "groups" in server_config - - # Verify env configuration - env_config = server_config["env"] - assert isinstance(env_config, dict) - assert "CLOUD_PROFILE" in env_config - assert "CLOUD_REGION" in env_config - assert "API_KEY" in env_config - - # Verify the API_KEY uses substitution pattern - assert env_config["API_KEY"].startswith("${") - assert env_config["API_KEY"].endswith("}") + if __name__ == "__main__":