From f2126abe24591cf11bac9b72c6335321b1db4099 Mon Sep 17 00:00:00 2001 From: ukpratik Date: Sun, 15 Mar 2026 17:41:33 +0530 Subject: [PATCH] updating documentation & adding env var ref for model_list as well --- docs/tools_configuration.md | 37 +++++++++++++-- pkg/config/config.go | 16 ++++++- pkg/config/model_config_test.go | 82 +++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/docs/tools_configuration.md b/docs/tools_configuration.md index 8c8eb31f0..db9ecacef 100644 --- a/docs/tools_configuration.md +++ b/docs/tools_configuration.md @@ -150,10 +150,37 @@ and injected into the context for a configured number of turns (`ttl`). | `type` | string | no | Transport type: `stdio`, `sse`, `http` | | `command` | string | stdio | Executable command 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 | | `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 @@ -236,7 +263,7 @@ dynamically only when requested by the user.* "@modelcontextprotocol/server-github" ], "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_TOKEN" + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" } }, "postgres": { @@ -256,8 +283,8 @@ dynamically only when requested by the user.* "@modelcontextprotocol/server-slack" ], "env": { - "SLACK_BOT_TOKEN": "YOUR_SLACK_BOT_TOKEN", - "SLACK_TEAM_ID": "YOUR_SLACK_TEAM_ID" + "SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}", + "SLACK_TEAM_ID": "${SLACK_TEAM_ID}" } } } diff --git a/pkg/config/config.go b/pkg/config/config.go index f85323344..7a51236a1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -926,8 +926,22 @@ func applyModelEnvOverrides(cfg *Config) { "ollama": "OLLAMA_API_KEY", } 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 != "" { - continue // already set in config.json, don't override + continue } provider, _, ok := strings.Cut(m.Model, "/") if !ok { diff --git a/pkg/config/model_config_test.go b/pkg/config/model_config_test.go index da6e506f8..0d844e649 100644 --- a/pkg/config/model_config_test.go +++ b/pkg/config/model_config_test.go @@ -398,3 +398,85 @@ func TestModelConfig_RequestTimeoutDefaultZeroValue(t *testing.T) { 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") + } +}