From 4178ae8524c7a97f16a3b13711e83773b7db2f9b Mon Sep 17 00:00:00 2001 From: SIDDARTHA REDDY <75976672+SIDDARTHAREDDY8@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:09:20 -0400 Subject: [PATCH] fix(terraform): add sensitive auth_value credential field for litellm_mcp_server Accepting bearer_token/api_key in auth_type validation is not enough on its own: the provider had no way to send the credential, so those values could never work end to end. Add an optional, Sensitive auth_value field that is sent to the LiteLLM API as credentials.auth_value (matching NewMCPServerRequest), and omit the credentials blob entirely when unset. The field is never written back from API responses since the API redacts credentials on read. Signed-off-by: SIDDARTHA REDDY <75976672+SIDDARTHAREDDY8@users.noreply.github.com> --- .../provider/docs/resources/mcp_server.md | 10 +- .../provider/litellm/resource_mcp_server.go | 38 +++-- .../resource_mcp_server_auth_type_test.go | 138 ++++++++++++++++++ .../litellm/resource_mcp_server_crud.go | 7 + terraform/provider/litellm/types.go | 8 + 5 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 terraform/provider/litellm/resource_mcp_server_auth_type_test.go diff --git a/terraform/provider/docs/resources/mcp_server.md b/terraform/provider/docs/resources/mcp_server.md index 77457a5ae55..f87a7dff1bd 100644 --- a/terraform/provider/docs/resources/mcp_server.md +++ b/terraform/provider/docs/resources/mcp_server.md @@ -21,7 +21,8 @@ resource "litellm_mcp_server" "github_server" { description = "GitHub MCP server for repository operations" url = "https://api.github.com/mcp" transport = "http" - auth_type = "bearer" + auth_type = "bearer_token" + auth_value = var.github_token # sensitive variable; never hardcode credentials mcp_access_groups = ["dev_team", "devops_team"] } @@ -36,7 +37,7 @@ resource "litellm_mcp_server" "zapier_server" { description = "Zapier MCP server for workflow automation" url = "https://actions.zapier.com/mcp/sk-xxxxx/sse" transport = "sse" - auth_type = "bearer" + auth_type = "bearer_token" spec_version = "2024-11-05" mcp_access_groups = ["automation_team", "marketing_team"] @@ -104,7 +105,7 @@ resource "litellm_mcp_server" "enterprise_api_server" { description = "Enterprise API gateway MCP server" url = "https://api.enterprise.com/mcp/v1" transport = "http" - auth_type = "bearer" + auth_type = "bearer_token" spec_version = "2024-11-05" mcp_access_groups = [ @@ -149,7 +150,8 @@ The following arguments are supported: * `alias` - (Optional) Alias for the MCP server. Used for easier reference. * `description` - (Optional) Description of the MCP server. * `spec_version` - (Optional) MCP specification version. Defaults to `2024-11-05`. -* `auth_type` - (Optional) Authentication type. Valid values: `none`, `bearer`, `basic`. Defaults to `none`. +* `auth_type` - (Optional) Authentication type. Must be a LiteLLM MCPAuth value: `none`, `api_key`, `bearer_token`, `basic`, `authorization`, `oauth2`, `aws_sigv4`, `token`, `oauth2_token_exchange`, `oauth2_id_jag`, `true_passthrough`, `oauth_delegate`. Defaults to `none`. Use `bearer_token` for bearer auth. +* `auth_value` - (Optional, Sensitive) Authentication credential sent to the LiteLLM API as `credentials.auth_value` (e.g. the bearer token for `auth_type = "bearer_token"`, the API key for `auth_type = "api_key"`). Hidden from plan output. Only needed when the chosen `auth_type` requires a credential. * `mcp_access_groups` - (Optional) List of access groups that can use this MCP server. * `command` - (Optional) Command to run for stdio transport. * `args` - (Optional) List of arguments for the command (stdio transport only). Do not pass secrets as arguments; args are shown in plans, stored unencrypted in state, and visible in the server's process list. diff --git a/terraform/provider/litellm/resource_mcp_server.go b/terraform/provider/litellm/resource_mcp_server.go index 318925c4367..a1eec0e1891 100644 --- a/terraform/provider/litellm/resource_mcp_server.go +++ b/terraform/provider/litellm/resource_mcp_server.go @@ -5,6 +5,24 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +// mcpServerAuthTypes mirrors the LiteLLM API's MCPAuth enum (litellm/types/mcp.py). +// Values are sent to the API verbatim, so the provider must only accept the +// API's canonical values. Note the API expects "bearer_token", not "bearer". +var mcpServerAuthTypes = []string{ + "none", + "api_key", + "bearer_token", + "basic", + "authorization", + "oauth2", + "aws_sigv4", + "token", + "oauth2_token_exchange", + "oauth2_id_jag", + "true_passthrough", + "oauth_delegate", +} + func resourceLiteLLMMCPServer() *schema.Resource { return &schema.Resource{ Create: resourceLiteLLMMCPServerCreate, @@ -53,15 +71,17 @@ func resourceLiteLLMMCPServer() *schema.Resource { Description: "MCP specification version", }, "auth_type": { - Type: schema.TypeString, - Optional: true, - Default: "none", - ValidateFunc: validation.StringInSlice([]string{ - "none", - "bearer", - "basic", - }, false), - Description: "Authentication type (none, bearer, basic)", + Type: schema.TypeString, + Optional: true, + Default: "none", + ValidateFunc: validation.StringInSlice(mcpServerAuthTypes, false), + Description: "Authentication type. Must be a LiteLLM MCPAuth value (none, api_key, bearer_token, basic, authorization, oauth2, aws_sigv4, token, oauth2_token_exchange, oauth2_id_jag, true_passthrough, oauth_delegate). Use bearer_token for bearer auth. Defaults to none", + }, + "auth_value": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Authentication credential sent to the LiteLLM API as credentials.auth_value (e.g. the bearer token for auth_type \"bearer_token\", the API key for auth_type \"api_key\"). Sensitive: hidden from plan output. Only needed when the chosen auth_type requires a credential; ignored when unset.", }, "mcp_access_groups": { Type: schema.TypeList, diff --git a/terraform/provider/litellm/resource_mcp_server_auth_type_test.go b/terraform/provider/litellm/resource_mcp_server_auth_type_test.go new file mode 100644 index 00000000000..9dc3aa70425 --- /dev/null +++ b/terraform/provider/litellm/resource_mcp_server_auth_type_test.go @@ -0,0 +1,138 @@ +package litellm + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// Regression test for https://github.com/BerriAI/litellm/issues/43019. +// The provider used to validate auth_type against ["none", "bearer", "basic"], +// but the LiteLLM API's MCPAuth enum (litellm/types/mcp.py) expects +// "bearer_token" among its canonical values. auth_type="bearer" passed +// provider validation and then failed with a 422 from the API, while +// auth_type="bearer_token" failed provider plan validation. The provider now +// accepts the API's canonical MCPAuth values verbatim. +func TestMCPServerAuthTypeValidationAcceptsCanonicalMCPAuthValues(t *testing.T) { + vf := resourceLiteLLMMCPServer().Schema["auth_type"].ValidateFunc + if vf == nil { + t.Fatal("auth_type has no ValidateFunc") + } + + valid := []string{ + "none", + "api_key", + "bearer_token", + "basic", + "authorization", + "oauth2", + "aws_sigv4", + "token", + "oauth2_token_exchange", + "oauth2_id_jag", + "true_passthrough", + "oauth_delegate", + } + for _, v := range valid { + if _, errs := vf(v, "auth_type"); len(errs) > 0 { + t.Errorf("auth_type %q rejected, want accepted: %v", v, errs) + } + } +} + +func TestMCPServerAuthTypeValidationRejectsNonCanonicalValues(t *testing.T) { + vf := resourceLiteLLMMCPServer().Schema["auth_type"].ValidateFunc + if vf == nil { + t.Fatal("auth_type has no ValidateFunc") + } + + // "bearer" is not a LiteLLM MCPAuth value; the API returns 422 for it, so + // it must fail provider-side validation with a clear error instead of + // reaching the API. + invalid := []string{"bearer", "Bearer_Token", "BEARER", "jwt", "", " bearer_token"} + for _, v := range invalid { + if _, errs := vf(v, "auth_type"); len(errs) == 0 { + t.Errorf("auth_type %q accepted, want rejected", v) + } + } +} + +// The default must remain a valid value so existing configs without an +// explicit auth_type still plan cleanly. +func TestMCPServerAuthTypeDefaultIsValid(t *testing.T) { + r := resourceLiteLLMMCPServer() + def := r.Schema["auth_type"].Default.(string) + if def != "none" { + t.Fatalf("auth_type default = %q, want %q", def, "none") + } + if _, errs := r.Schema["auth_type"].ValidateFunc(def, "auth_type"); len(errs) > 0 { + t.Errorf("auth_type default %q rejected: %v", def, errs) + } +} + +// auth_value carries the credential for auth types that need one (e.g. +// bearer_token, api_key). It must be optional and sensitive: hidden from plan +// output and never written back from API responses (the API redacts +// credentials on read, same as the sensitive "env" field). +func TestMCPServerAuthValueIsOptionalAndSensitive(t *testing.T) { + f, ok := resourceLiteLLMMCPServer().Schema["auth_value"] + if !ok { + t.Fatal("schema has no auth_value field") + } + if !f.Optional { + t.Error("auth_value should be Optional") + } + if !f.Sensitive { + t.Error("auth_value must be Sensitive so the credential never appears in plan output") + } +} + +// The configured credential must reach the API as credentials.auth_value, +// matching the LiteLLM API's NewMCPServerRequest shape. +func TestMCPServerBuildRequestSendsAuthValueAsCredentials(t *testing.T) { + d := schema.TestResourceDataRaw(t, resourceLiteLLMMCPServer().Schema, map[string]interface{}{ + "server_name": "s", + "url": "https://example.com/mcp", + "transport": "http", + "auth_type": "bearer_token", + "auth_value": "secret-token", + }) + req := buildMCPServerRequest(d) + if req.Credentials == nil { + t.Fatal("expected Credentials to be set when auth_value is configured") + } + if req.Credentials.AuthValue != "secret-token" { + t.Errorf("Credentials.AuthValue = %q, want %q", req.Credentials.AuthValue, "secret-token") + } + body, err := json.Marshal(req) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + if !strings.Contains(string(body), `"credentials":{"auth_value":"secret-token"}`) { + t.Errorf("request JSON missing credentials.auth_value, got: %s", body) + } +} + +// No credentials blob may be sent when auth_value is unset, so existing +// configs and auth_type="none" behave exactly as before. +func TestMCPServerBuildRequestOmitsCredentialsWhenAuthValueUnset(t *testing.T) { + d := schema.TestResourceDataRaw(t, resourceLiteLLMMCPServer().Schema, map[string]interface{}{ + "server_name": "s", + "url": "https://example.com/mcp", + "transport": "http", + "auth_type": "none", + }) + req := buildMCPServerRequest(d) + if req.Credentials != nil { + t.Errorf("expected no Credentials when auth_value is unset, got %+v", req.Credentials) + } + body, err := json.Marshal(req) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + if strings.Contains(string(body), "credentials") { + t.Errorf("request JSON must not contain credentials when auth_value is unset, got: %s", body) + } +} diff --git a/terraform/provider/litellm/resource_mcp_server_crud.go b/terraform/provider/litellm/resource_mcp_server_crud.go index 2a8980960f1..3cd39f253cb 100644 --- a/terraform/provider/litellm/resource_mcp_server_crud.go +++ b/terraform/provider/litellm/resource_mcp_server_crud.go @@ -25,6 +25,13 @@ func buildMCPServerRequest(d *schema.ResourceData) *MCPServerRequest { AuthType: d.Get("auth_type").(string), } + // Set the credential when one is configured. The API redacts credentials + // on read, so auth_value is never written back from responses (same as + // the sensitive "env" field): the configured value stays in state. + if authValue, ok := d.GetOk("auth_value"); ok { + req.Credentials = &MCPCredentials{AuthValue: authValue.(string)} + } + // Set optional fields if alias, ok := d.GetOk("alias"); ok { req.Alias = alias.(string) diff --git a/terraform/provider/litellm/types.go b/terraform/provider/litellm/types.go index a8784b8a6a9..878074696b0 100644 --- a/terraform/provider/litellm/types.go +++ b/terraform/provider/litellm/types.go @@ -193,6 +193,7 @@ type MCPServerRequest struct { Transport string `json:"transport"` SpecVersion string `json:"spec_version,omitempty"` AuthType string `json:"auth_type,omitempty"` + Credentials *MCPCredentials `json:"credentials,omitempty"` URL string `json:"url"` MCPInfo *MCPInfo `json:"mcp_info,omitempty"` MCPAccessGroups []string `json:"mcp_access_groups,omitempty"` @@ -201,6 +202,13 @@ type MCPServerRequest struct { Env map[string]string `json:"env,omitempty"` } +// MCPCredentials mirrors litellm.types.mcp.MCPCredentials: the credential +// blob accepted by the LiteLLM API's NewMCPServerRequest. Only the fields the +// provider sets are modeled here. +type MCPCredentials struct { + AuthValue string `json:"auth_value,omitempty"` +} + // MCPServerResponse represents a response from the API containing MCP server information. type MCPServerResponse struct { ServerID string `json:"server_id"`