Skip to content

Commit 4b7ffc8

Browse files
authored
Merge pull request #5 from o3-cloud/claude/issue-4-20250715-2236
Add environment variable support for MCP secrets
2 parents ad9d59a + 05711ee commit 4b7ffc8

File tree

3 files changed

+86
-8
lines changed

3 files changed

+86
-8
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,13 @@ Get your first AI agent running in under 2 minutes:
4545

4646
```bash
4747
# 1. Install Agentman
48+
49+
# From PyPI (recommended)
4850
pip install agentman-mcp
4951

52+
# Or, install the latest version from GitHub using uv
53+
uv tool install git+https://github.com/o3-cloud/agentman.git@main#egg=agentman-mcp
54+
5055
# 2. Create and run your first agent
5156
mkdir my-agent && cd my-agent
5257
agentman run --from-agentfile -t my-agent .
@@ -215,6 +220,32 @@ The intuitive `Agentfile` syntax lets you focus on designing intelligent workflo
215220
| **🔐 Secure Secrets** | Environment-based secret handling with templates |
216221
| **🧪 Battle-Tested** | 91%+ test coverage ensures reliability |
217222

223+
### ✨ Environment Variable Expansion in Agentfiles
224+
225+
Now you can use environment variables directly in your `Agentfile` and `Agentfile.yml` for more flexible and secure configurations.
226+
227+
**Usage examples:**
228+
229+
**Agentfile format**
230+
```dockerfile
231+
# Agentfile format
232+
SECRET ALIYUN_API_KEY ${ALIYUN_API_KEY}
233+
MCP_SERVER github-mcp-server
234+
ENV GITHUB_PERSONAL_ACCESS_TOKEN ${GITHUB_TOKEN}
235+
```
236+
237+
**YAML format**
238+
```yaml
239+
# YAML format
240+
secrets:
241+
- name: ALIYUN_API_KEY
242+
value: ${ALIYUN_API_KEY}
243+
mcp_servers:
244+
- name: github-mcp-server
245+
env:
246+
GITHUB_PERSONAL_ACCESS_TOKEN: ${GITHUB_TOKEN}
247+
```
248+
218249
### 🌟 What Makes Agentman Different?
219250
220251
**Traditional AI Development:**
@@ -744,7 +775,7 @@ agentman/
744775
## 🏗️ Building from Source
745776

746777
```bash
747-
git clone https://github.com/yeahdongcn/agentman.git
778+
git clone https://github.com/o3-cloud/agentman.git
748779
cd agentman
749780

750781
# Install

src/agentman/agentfile_parser.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,43 @@
11
"""Agentfile parser module for parsing Agentfile configurations."""
22

33
import json
4+
import os
5+
import re
46
from dataclasses import dataclass, field
57
from typing import Any, Dict, List, Optional, Union
68

79

10+
def expand_env_vars(value: str) -> str:
11+
"""
12+
Expand environment variables in a string.
13+
14+
Supports both ${VAR} and $VAR syntax.
15+
If environment variable is not found, returns the original placeholder.
16+
17+
Args:
18+
value: String that may contain environment variable references
19+
20+
Returns:
21+
String with environment variables expanded
22+
"""
23+
if not isinstance(value, str):
24+
return value
25+
26+
# Pattern to match ${VAR} or $VAR (where VAR is alphanumeric + underscore)
27+
pattern = r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)'
28+
29+
def replace_var(match):
30+
# Get the variable name from either group
31+
var_name = match.group(1) or match.group(2)
32+
env_value = os.environ.get(var_name)
33+
if env_value is not None:
34+
return env_value
35+
# Return the original placeholder if env var not found
36+
return match.group(0)
37+
38+
return re.sub(pattern, replace_var, value)
39+
40+
841
@dataclass
942
class MCPServer:
1043
"""Represents an MCP server configuration."""
@@ -516,7 +549,8 @@ def _handle_secret(self, parts: List[str]):
516549
# Check if it's an inline value: SECRET KEY value
517550
if len(parts) >= 3:
518551
value = ' '.join(parts[2:]) # Join all remaining parts as the value
519-
secret = SecretValue(name=secret_name, value=self._unquote(value))
552+
expanded_value = expand_env_vars(self._unquote(value))
553+
secret = SecretValue(name=secret_name, value=expanded_value)
520554
self.config.secrets.append(secret)
521555
self.current_context = None
522556
# Check if it's a context (no value, will be populated with sub-instructions)
@@ -565,7 +599,8 @@ def _handle_secret_sub_instruction(self, instruction: str, parts: List[str]):
565599
if len(parts) >= 2:
566600
key = instruction.upper()
567601
value = ' '.join(parts[1:])
568-
secret_context.values[key] = self._unquote(value)
602+
expanded_value = expand_env_vars(self._unquote(value))
603+
secret_context.values[key] = expanded_value
569604
else:
570605
raise ValueError("SECRET context requires KEY VALUE format")
571606

@@ -680,14 +715,16 @@ def _handle_server_sub_instruction(self, instruction: str, parts: List[str]):
680715
key, value = env_part.split('=', 1) # Split only on first =
681716
key = self._unquote(key)
682717
value = self._unquote(value)
683-
server.env[key] = value
718+
expanded_value = expand_env_vars(value)
719+
server.env[key] = expanded_value
684720
else:
685721
raise ValueError("ENV requires KEY VALUE or KEY=VALUE")
686722
elif len(parts) >= 3:
687723
# Handle KEY VALUE format
688724
key = self._unquote(parts[1])
689725
value = self._unquote(' '.join(parts[2:])) # Join remaining parts as value
690-
server.env[key] = value
726+
expanded_value = expand_env_vars(value)
727+
server.env[key] = expanded_value
691728
else:
692729
raise ValueError("ENV requires KEY VALUE or KEY=VALUE")
693730

src/agentman/yaml_parser.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
Router,
1717
SecretContext,
1818
SecretValue,
19+
expand_env_vars,
1920
)
2021

2122

@@ -135,7 +136,11 @@ def _parse_mcp_servers(self, servers_config: List[Dict[str, Any]]):
135136
if 'env' in server_config:
136137
env = server_config['env']
137138
if isinstance(env, dict):
138-
server.env = env
139+
# Expand environment variables in values
140+
expanded_env = {}
141+
for key, value in env.items():
142+
expanded_env[key] = expand_env_vars(value)
143+
server.env = expanded_env
139144
else:
140145
raise ValueError("MCP server 'env' must be a dictionary")
141146

@@ -299,13 +304,18 @@ def _parse_secrets(self, secrets_config: List[Union[str, Dict[str, Any]]]):
299304

300305
if 'value' in secret_config:
301306
# Inline secret value
302-
secret = SecretValue(name=name, value=secret_config['value'])
307+
expanded_value = expand_env_vars(secret_config['value'])
308+
secret = SecretValue(name=name, value=expanded_value)
303309
self.config.secrets.append(secret)
304310
elif 'values' in secret_config:
305311
# Secret context with multiple values
306312
values = secret_config['values']
307313
if isinstance(values, dict):
308-
secret = SecretContext(name=name, values=values)
314+
# Expand environment variables in values
315+
expanded_values = {}
316+
for key, value in values.items():
317+
expanded_values[key] = expand_env_vars(value)
318+
secret = SecretContext(name=name, values=expanded_values)
309319
self.config.secrets.append(secret)
310320
else:
311321
raise ValueError("Secret 'values' must be a dictionary")

0 commit comments

Comments
 (0)