updating documentation & adding env var ref for model_list as well

This commit is contained in:
ukpratik 2026-03-15 17:41:33 +05:30
parent 2be28f1c21
commit f2126abe24
3 changed files with 129 additions and 6 deletions

View file

@ -150,10 +150,37 @@ and injected into the context for a configured number of turns (`ttl`).
| `type` | string | no | Transport type: `stdio`, `sse`, `http` | | `type` | string | no | Transport type: `stdio`, `sse`, `http` |
| `command` | string | stdio | Executable command for stdio transport | | `command` | string | stdio | Executable command for stdio transport |
| `args` | array | no | Command arguments for stdio transport | | `args` | array | no | Command arguments for stdio transport |
| `env` | object | no | Environment variables for stdio process | | `env` | object | no | Environment variables for stdio process (supports `${VAR}` references) |
| `env_file` | string | no | Path to environment file for stdio process | | `env_file` | string | no | Path to environment file for stdio process |
| `url` | string | sse/http | Endpoint URL for `sse`/`http` transport | | `url` | string | sse/http | Endpoint URL for `sse`/`http` transport |
| `headers` | object | no | HTTP headers for `sse`/`http` transport | | `headers` | object | no | HTTP headers for `sse`/`http` transport (supports `${VAR}` references) |
### Secrets via `.env` File
Values in `env` and `headers` maps support `${VAR_NAME}` substitution. At startup, any value that is exactly `${VAR_NAME}` is replaced with the corresponding environment variable, which can come from a `.env` file in the working directory or from the shell environment.
This keeps secrets out of `config.json` (which may be committed to source control):
```json
"github": {
"enabled": true,
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
}
```
```sh
# .env (git-ignored)
GITHUB_PERSONAL_ACCESS_TOKEN=ghp_your_actual_token
```
**Rules:**
- Only whole-value references are substituted: `"${FOO}"` → resolved. `"prefix-${FOO}"` → unchanged.
- If the referenced variable is unset, the original `${VAR_NAME}` string is left as-is.
- Literal values (no `${}`) pass through unchanged — existing configs continue to work.
### Transport Behavior ### Transport Behavior
@ -236,7 +263,7 @@ dynamically only when requested by the user.*
"@modelcontextprotocol/server-github" "@modelcontextprotocol/server-github"
], ],
"env": { "env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_TOKEN" "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
} }
}, },
"postgres": { "postgres": {
@ -256,8 +283,8 @@ dynamically only when requested by the user.*
"@modelcontextprotocol/server-slack" "@modelcontextprotocol/server-slack"
], ],
"env": { "env": {
"SLACK_BOT_TOKEN": "YOUR_SLACK_BOT_TOKEN", "SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
"SLACK_TEAM_ID": "YOUR_SLACK_TEAM_ID" "SLACK_TEAM_ID": "${SLACK_TEAM_ID}"
} }
} }
} }

View file

@ -926,8 +926,22 @@ func applyModelEnvOverrides(cfg *Config) {
"ollama": "OLLAMA_API_KEY", "ollama": "OLLAMA_API_KEY",
} }
for i, m := range cfg.ModelList { for i, m := range cfg.ModelList {
// Resolve ${VAR} references in sensitive/configurable fields first.
// This supports custom providers and private endpoints without hardcoding secrets.
if resolved := resolveEnvRef(m.APIKey); resolved != m.APIKey {
cfg.ModelList[i].APIKey = resolved
m.APIKey = resolved
}
if resolved := resolveEnvRef(m.APIBase); resolved != m.APIBase {
cfg.ModelList[i].APIBase = resolved
}
if resolved := resolveEnvRef(m.Proxy); resolved != m.Proxy {
cfg.ModelList[i].Proxy = resolved
}
// Fall back to well-known provider env var if api_key is still empty.
if m.APIKey != "" { if m.APIKey != "" {
continue // already set in config.json, don't override continue
} }
provider, _, ok := strings.Cut(m.Model, "/") provider, _, ok := strings.Cut(m.Model, "/")
if !ok { if !ok {

View file

@ -398,3 +398,85 @@ func TestModelConfig_RequestTimeoutDefaultZeroValue(t *testing.T) {
t.Fatalf("RequestTimeout = %d, want 0", cfg.RequestTimeout) t.Fatalf("RequestTimeout = %d, want 0", cfg.RequestTimeout)
} }
} }
func TestApplyModelEnvOverrides_ExplicitVarRef_APIKey(t *testing.T) {
t.Setenv("MY_CUSTOM_API_KEY", "sk-custom-resolved")
cfg := &Config{
ModelList: []ModelConfig{
{ModelName: "custom", Model: "myhost/my-model", APIKey: "${MY_CUSTOM_API_KEY}"},
},
}
applyModelEnvOverrides(cfg)
if got := cfg.ModelList[0].APIKey; got != "sk-custom-resolved" {
t.Errorf("APIKey = %q, want %q", got, "sk-custom-resolved")
}
}
func TestApplyModelEnvOverrides_ExplicitVarRef_APIBase(t *testing.T) {
t.Setenv("MY_PRIVATE_ENDPOINT", "https://private.example.com/v1")
cfg := &Config{
ModelList: []ModelConfig{
{ModelName: "local", Model: "openai/gpt-4o", APIKey: "key", APIBase: "${MY_PRIVATE_ENDPOINT}"},
},
}
applyModelEnvOverrides(cfg)
if got := cfg.ModelList[0].APIBase; got != "https://private.example.com/v1" {
t.Errorf("APIBase = %q, want %q", got, "https://private.example.com/v1")
}
}
func TestApplyModelEnvOverrides_ExplicitVarRef_Proxy(t *testing.T) {
t.Setenv("MY_PROXY", "http://proxy.example.com:8080")
cfg := &Config{
ModelList: []ModelConfig{
{ModelName: "proxied", Model: "openai/gpt-4o", APIKey: "key", Proxy: "${MY_PROXY}"},
},
}
applyModelEnvOverrides(cfg)
if got := cfg.ModelList[0].Proxy; got != "http://proxy.example.com:8080" {
t.Errorf("Proxy = %q, want %q", got, "http://proxy.example.com:8080")
}
}
func TestApplyModelEnvOverrides_VarRef_ThenProviderFallback(t *testing.T) {
// ${VAR} on api_key resolves to empty (unset) → provider fallback should still apply
t.Setenv("OPENAI_API_KEY", "sk-from-provider-map")
cfg := &Config{
ModelList: []ModelConfig{
// api_key is empty — should be filled by provider map fallback
{ModelName: "gpt4", Model: "openai/gpt-4o", APIKey: ""},
},
}
applyModelEnvOverrides(cfg)
if got := cfg.ModelList[0].APIKey; got != "sk-from-provider-map" {
t.Errorf("APIKey = %q, want %q (provider fallback should apply)", got, "sk-from-provider-map")
}
}
func TestApplyModelEnvOverrides_HardcodedAPIKey_NotOverridden(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "sk-from-env")
cfg := &Config{
ModelList: []ModelConfig{
{ModelName: "gpt4", Model: "openai/gpt-4o", APIKey: "sk-hardcoded"},
},
}
applyModelEnvOverrides(cfg)
if got := cfg.ModelList[0].APIKey; got != "sk-hardcoded" {
t.Errorf("APIKey = %q, want %q (hardcoded value must not be overridden)", got, "sk-hardcoded")
}
}