Skip to content

Commit 32fe4b5

Browse files
authored
Merge pull request #31 from Model-Context-Interface/copilot/auto-parse-env-files
Auto-parse .env and .env.mci files from project root and ./mci directory using python-dotenv
2 parents 41f17a8 + d762f44 commit 32fe4b5

File tree

10 files changed

+1540
-13
lines changed

10 files changed

+1540
-13
lines changed

README.md

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -580,9 +580,136 @@ MCI supports environment variable templating in tool definitions:
580580
}
581581
```
582582

583+
### Automatic .env File Loading
584+
585+
MCI automatically loads environment variables from `.env` and `.env.mci` files when loading your MCI schema. This provides a convenient way to manage environment-specific configuration without manually setting variables.
586+
587+
**Automatic Detection:**
588+
- When you run any MCI command (`run`, `list`, `validate`, etc.), MCI looks for environment files in:
589+
1. The project root directory (same location as your `mci.json` or `mci.yaml`)
590+
2. The `./mci` directory
591+
592+
**File Priority:**
593+
- MCI prioritizes `.env.mci` files (MCI-specific configs) over `.env` files (general configs)
594+
- **Important:** When `.env.mci` files exist, `.env` files are **not loaded** at all
595+
- This allows you to keep MCI-specific environment variables separate from general project variables
596+
597+
**Loading Behavior:**
598+
- **If `.env.mci` files exist:** Only `.env.mci` files are loaded
599+
1. `./mci/.env.mci` - Library MCI-specific configs
600+
2. Project root `.env.mci` - Project MCI-specific configs (higher priority)
601+
- **If no `.env.mci` files exist:** `.env` files are loaded instead
602+
1. `./mci/.env` - Library general defaults
603+
2. Project root `.env` - Project-level configs (higher priority)
604+
- **Then:** System environment variables and explicit env_vars override all file-based configs
605+
606+
**Full Precedence Order (lowest to highest):**
607+
1. File-based configs (`.env.mci` files if they exist, otherwise `.env` files)
608+
- From `./mci/` directory first
609+
- Then from project root
610+
2. System environment variables (set via `export` or shell config)
611+
3. Environment variables passed via CLI or code (highest priority)
612+
613+
**Example .env file:**
614+
```bash
615+
# .env or .env.mci file in project root
616+
API_KEY=your-api-key-here
617+
BASE_URL=https://api.example.com
618+
DEBUG=false
619+
620+
# Comments are supported
621+
# Blank lines are ignored
622+
623+
# Export keyword is optional (automatically stripped)
624+
export OPTIONAL_VAR=value
625+
```
626+
627+
**Example directory structure:**
628+
```
629+
my-project/
630+
├── .env.mci # MCI-specific variables (if present, .env ignored)
631+
├── .env # General project variables (only if .env.mci absent)
632+
├── mci.json # Your MCI schema
633+
└── mci/
634+
├── .env.mci # Library MCI-specific (if present, ./mci/.env ignored)
635+
├── .env # Library defaults (only if ./mci/.env.mci absent)
636+
└── weather.mci.json # Your toolsets
637+
```
638+
639+
**Example: Using .env.mci files**
640+
```bash
641+
# Scenario 1: Only .env.mci files present
642+
# ./mci/.env.mci
643+
API_KEY=mci-library-key
644+
LIBRARY_VAR=lib-value
645+
646+
# .env.mci (project root)
647+
API_KEY=mci-project-key # Overrides ./mci/.env.mci
648+
MCI_SPECIFIC=mci-value
649+
650+
# Result: MCI will use:
651+
# API_KEY=mci-project-key (from root .env.mci)
652+
# LIBRARY_VAR=lib-value (from ./mci/.env.mci)
653+
# MCI_SPECIFIC=mci-value (from root .env.mci)
654+
```
655+
656+
**Example: Using .env files (no .env.mci)**
657+
```bash
658+
# Scenario 2: Only .env files present
659+
# ./mci/.env
660+
API_KEY=default-key
661+
LIBRARY_VAR=lib-value
662+
663+
# .env (project root)
664+
API_KEY=project-key # Overrides ./mci/.env
665+
PROJECT_VAR=proj-value
666+
667+
# Result: MCI will use:
668+
# API_KEY=project-key (from root .env)
669+
# LIBRARY_VAR=lib-value (from ./mci/.env)
670+
# PROJECT_VAR=proj-value (from root .env)
671+
```
672+
673+
**Notes:**
674+
- .env files are completely optional - MCI works fine without them
675+
- No error if .env files are missing
676+
- Use `.env.mci` for MCI-specific configurations to keep them completely separate from general project configs
677+
- When `.env.mci` exists, `.env` is ignored (not merged)
678+
- You can disable auto-loading by setting `auto_load_dotenv=False` when using the MCI library programmatically
679+
- .env files should NOT be committed to version control if they contain secrets
680+
583681
### Setting Environment Variables for MCP Clients
584682

585-
When running MCI as an MCP server from clients like Claude Desktop or VS Code, configure environment variables in the client's settings:
683+
When running MCI as an MCP server from clients like Claude Desktop or VS Code, you have two options for environment variables:
684+
685+
1. **Use .env files** (recommended): Create a `.env` file in your project root, and MCI will automatically load it
686+
2. **Configure in client settings**: Explicitly set environment variables in the MCP client configuration
687+
688+
**Option 1: Using .env files (Recommended)**
689+
690+
Create a `.env` file in your project root:
691+
```bash
692+
# .env
693+
API_KEY=your-api-key
694+
BASE_URL=https://api.example.com
695+
```
696+
697+
Then configure your MCP client with minimal settings:
698+
```json
699+
{
700+
"mcpServers": {
701+
"mci-tools": {
702+
"command": "uvx",
703+
"args": ["mcix", "run"],
704+
"cwd": "/path/to/your/project"
705+
}
706+
}
707+
}
708+
```
709+
710+
MCI will automatically load the `.env` file when it starts.
711+
712+
**Option 2: Configure in Client Settings**
586713

587714
**Claude Desktop Example** (`claude_desktop_config.json`):
588715
```json
@@ -622,14 +749,23 @@ When running MCI as an MCP server from clients like Claude Desktop or VS Code, c
622749

623750
**Running Standalone** (without MCP client):
624751

625-
If you're running the MCP server directly in a terminal, set environment variables first:
752+
MCI automatically loads `.env` files from your project root and `./mci` directory:
753+
754+
```bash
755+
# Create .env file
756+
echo "API_KEY=your-api-key" > .env
757+
echo "BASE_URL=https://api.example.com" >> .env
626758

759+
# Run MCI - it will automatically load .env
760+
uvx mcix run
761+
```
762+
763+
You can also set environment variables manually if needed:
627764
```bash
628765
export API_KEY=your-api-key
629766
export BASE_URL=https://api.example.com
630767
uvx mcix run
631768
```
632-
```
633769

634770
## Integration with MCP Clients
635771

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies = [
4242
"mci-py>=1.1.4",
4343
"mcp>=1.19.0",
4444
"pytest-cov>=7.0.0",
45+
"python-dotenv>=1.2.1",
4546
"pyyaml>=6.0.3",
4647
"rich>=14.2.0",
4748
]

src/mci/core/config.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
44
This module provides functionality to load and validate MCI configuration files
55
using the MCIClient from mci-py. It handles schema validation, error handling,
6-
and provides user-friendly error messages.
6+
and provides user-friendly error messages. It also automatically loads environment
7+
variables from .env files in the project root and ./mci directory.
78
"""
89

10+
from pathlib import Path
11+
912
from mcipy import MCIClient, MCIClientError
1013

14+
from mci.utils.dotenv import get_env_with_dotenv
15+
1116

1217
class MCIConfig:
1318
"""
@@ -17,19 +22,40 @@ class MCIConfig:
1722
MCIClient from mci-py, which performs built-in schema validation.
1823
It also provides utilities for validating schemas and extracting
1924
user-friendly error messages.
25+
26+
The class automatically loads environment variables from .env files in:
27+
- The project root directory (same location as the MCI schema file)
28+
- The ./mci directory
29+
30+
Variables are merged with project root taking precedence over ./mci/.env.
2031
"""
2132

2233
@staticmethod
23-
def load(file_path: str, env_vars: dict[str, str] | None = None) -> MCIClient:
34+
def load(
35+
file_path: str, env_vars: dict[str, str] | None = None, auto_load_dotenv: bool = True
36+
) -> MCIClient:
2437
"""
2538
Load and parse an MCI configuration file using MCIClient.
2639
2740
This method uses MCIClient from mci-py to load and validate the schema.
2841
The MCIClient performs comprehensive schema validation during initialization.
2942
43+
If auto_load_dotenv is True (default), automatically loads environment variables
44+
from .env and .env.mci files. Priority order:
45+
- If .env.mci files exist:
46+
1. ./mci/.env.mci (library MCI-specific)
47+
2. Project root .env.mci (project MCI-specific)
48+
- If no .env.mci files exist:
49+
1. ./mci/.env (library defaults)
50+
2. Project root .env (project-level)
51+
- Then:
52+
3. System environment variables
53+
4. env_vars argument (highest priority)
54+
3055
Args:
3156
file_path: Path to the MCI schema file (.json, .yaml, or .yml)
32-
env_vars: Optional environment variables for template substitution
57+
env_vars: Optional environment variables for template substitution (highest priority)
58+
auto_load_dotenv: Whether to automatically load .env files (default: True)
3359
3460
Returns:
3561
An initialized MCIClient instance
@@ -41,20 +67,33 @@ def load(file_path: str, env_vars: dict[str, str] | None = None) -> MCIClient:
4167
Example:
4268
>>> config = MCIConfig()
4369
>>> try:
70+
... # Auto-loads .env files from project root and ./mci
4471
... client = config.load("mci.json")
4572
... tools = client.tools()
4673
... except MCIClientError as e:
4774
... print(f"Schema invalid: {e}")
4875
"""
4976
try:
50-
client = MCIClient(schema_file_path=file_path, env_vars=env_vars or {})
77+
# Determine project root from schema file location
78+
project_root = Path(file_path).parent.resolve()
79+
80+
# Load environment variables with proper precedence
81+
if auto_load_dotenv:
82+
merged_env = get_env_with_dotenv(project_root, env_vars)
83+
else:
84+
# If auto-loading is disabled, just use provided env_vars
85+
merged_env = env_vars or {}
86+
87+
client = MCIClient(schema_file_path=file_path, env_vars=merged_env)
5188
return client
5289
except MCIClientError:
5390
# Re-raise with the original error message from mci-py
5491
raise
5592

5693
@staticmethod
57-
def validate_schema(file_path: str, env_vars: dict[str, str] | None = None) -> tuple[bool, str]:
94+
def validate_schema(
95+
file_path: str, env_vars: dict[str, str] | None = None, auto_load_dotenv: bool = True
96+
) -> tuple[bool, str]:
5897
"""
5998
Validate an MCI schema file using MCIClient.
6099
@@ -63,9 +102,13 @@ def validate_schema(file_path: str, env_vars: dict[str, str] | None = None) -> t
63102
MCIClient performs comprehensive validation including schema structure,
64103
required fields, and data types.
65104
105+
If auto_load_dotenv is True (default), automatically loads environment variables
106+
from .env files in the project root and ./mci directory.
107+
66108
Args:
67109
file_path: Path to the MCI schema file to validate
68110
env_vars: Optional environment variables for template substitution
111+
auto_load_dotenv: Whether to automatically load .env files (default: True)
69112
70113
Returns:
71114
A tuple of (is_valid, error_message) where:
@@ -79,9 +122,19 @@ def validate_schema(file_path: str, env_vars: dict[str, str] | None = None) -> t
79122
... print(f"Validation failed: {error}")
80123
"""
81124
try:
125+
# Determine project root from schema file location
126+
project_root = Path(file_path).parent.resolve()
127+
128+
# Load environment variables with proper precedence
129+
if auto_load_dotenv:
130+
merged_env = get_env_with_dotenv(project_root, env_vars)
131+
else:
132+
# If auto-loading is disabled, just use provided env_vars
133+
merged_env = env_vars or {}
134+
82135
# Use validating=True to skip template resolution for MCP servers
83136
# This allows validation without requiring all env_vars at validation time
84-
MCIClient(schema_file_path=file_path, env_vars=env_vars or {}, validating=True)
137+
MCIClient(schema_file_path=file_path, env_vars=merged_env, validating=True)
85138
return (True, "")
86139
except MCIClientError as e:
87140
return (False, str(e))

src/mci/core/mci_client.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
44
This module provides a CLI-friendly wrapper around the MCIClient class from mci-py.
55
It delegates all schema parsing, validation, tool loading, and filtering to MCIClient,
6-
while providing CLI-specific error handling and output formatting.
6+
while providing CLI-specific error handling and output formatting. It also supports
7+
automatic loading of environment variables from .env files.
78
"""
89

10+
from pathlib import Path
11+
912
from mcipy import MCIClient
1013
from mcipy.models import Tool
1114

15+
from mci.utils.dotenv import get_env_with_dotenv
16+
1217

1318
class MCIClientWrapper:
1419
"""
@@ -17,20 +22,49 @@ class MCIClientWrapper:
1722
This class provides a CLI-friendly interface to MCIClient, delegating all
1823
tool loading, filtering, and schema operations to the upstream mci-py library.
1924
It focuses on error handling and formatting for CLI usability.
25+
26+
The wrapper automatically loads environment variables from .env files in:
27+
- The project root directory (same location as the MCI schema file)
28+
- The ./mci directory
2029
"""
2130

22-
def __init__(self, file_path: str, env_vars: dict[str, str] | None = None):
31+
def __init__(
32+
self, file_path: str, env_vars: dict[str, str] | None = None, auto_load_dotenv: bool = True
33+
):
2334
"""
2435
Initialize the wrapper with an MCIClient instance.
2536
37+
If auto_load_dotenv is True (default), automatically loads environment variables
38+
from .env and .env.mci files. Priority order:
39+
- If .env.mci files exist:
40+
1. ./mci/.env.mci (library MCI-specific)
41+
2. Project root .env.mci (project MCI-specific)
42+
- If no .env.mci files exist:
43+
1. ./mci/.env (library defaults)
44+
2. Project root .env (project-level)
45+
- Then:
46+
3. System environment variables
47+
4. env_vars argument (highest priority)
48+
2649
Args:
2750
file_path: Path to the MCI schema file (.json, .yaml, or .yml)
28-
env_vars: Optional environment variables for template substitution
51+
env_vars: Optional environment variables for template substitution (highest priority)
52+
auto_load_dotenv: Whether to automatically load .env files (default: True)
2953
3054
Raises:
3155
MCIClientError: If the schema file cannot be loaded or parsed
3256
"""
33-
self._client: MCIClient = MCIClient(schema_file_path=file_path, env_vars=env_vars or {})
57+
# Determine project root from schema file location
58+
project_root = Path(file_path).parent.resolve()
59+
60+
# Load environment variables with proper precedence
61+
if auto_load_dotenv:
62+
merged_env = get_env_with_dotenv(project_root, env_vars)
63+
else:
64+
# If auto-loading is disabled, just use provided env_vars
65+
merged_env = env_vars or {}
66+
67+
self._client: MCIClient = MCIClient(schema_file_path=file_path, env_vars=merged_env)
3468
self._file_path: str = file_path
3569

3670
@property

0 commit comments

Comments
 (0)