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>
This commit is contained in:
SIDDARTHA REDDY 2026-09-24 23:09:20 -04:00
parent f3cf1cdfef
commit 4178ae8524
5 changed files with 188 additions and 13 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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"`