BerriAI/litellm · error · ValueError
MCPJWTSigner: environment variable '{env_var}' is set but em
Error message
MCPJWTSigner: environment variable '{env_var}' is set but empty. What it means
MCPJWTSigner loads its RSA private key from MCP_JWT_SIGNING_KEY, which may hold a PEM string or a file:// path, and _load_private_key_from_env raises ValueError when the variable's value is empty. Note the guard only runs when the variable is present, so this specifically means 'set to an empty string'; when the variable is absent entirely the signer generates an ephemeral key instead.
Source
Thrown at litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py:129
_mcp_jwt_signer_instance: Optional["MCPJWTSigner"] = None
_MCP_JWT_CALL_TYPES: Final = frozenset({"call_mcp_tool", "list_mcp_tools"})
# Simple in-memory JWKS cache: keyed by JWKS URI → (keys_list, fetched_at).
_jwks_cache: Final[dict[str, tuple[Sequence[Mapping[str, object]], float]]] = {}
_JWKS_CACHE_TTL: Final = 3600 # 1 hour
def get_mcp_jwt_signer() -> Optional["MCPJWTSigner"]:
"""Return the active MCPJWTSigner singleton, or None if not initialized."""
return _mcp_jwt_signer_instance
def _load_private_key_from_env(env_var: str) -> RSAPrivateKey:
"""Load an RSA private key from an env var (PEM string or file:// path)."""
key_material: Final = os.environ.get(env_var, "")
if not key_material:
raise ValueError(f"MCPJWTSigner: environment variable '{env_var}' is set but empty.")
if key_material.startswith("file://"):
path: Final = key_material[len("file://") :]
with open(path, "rb") as f:
key_bytes = f.read()
else:
key_bytes = key_material.encode("utf-8")
return serialization.load_pem_private_key(key_bytes, password=None)
def _generate_rsa_key_pair() -> RSAPrivateKey:
"""Generate a new RSA-2048 private key."""
return rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
def _int_to_base64url(n: int) -> str:View on GitHub (pinned to 77b7c6c40c)
Solutions
- Populate MCP_JWT_SIGNING_KEY with a valid RSA private key in PEM form, or a file:// URL pointing at the PEM file
- If you do not need a stable key, unset MCP_JWT_SIGNING_KEY entirely - the signer then generates an ephemeral RSA-2048 key at startup
- Fix the secret source (Kubernetes secret name/key, vault path) so the variable receives a real value instead of an empty string
Example fix
# before - set but empty, raises ValueError at startup export MCP_JWT_SIGNING_KEY="" # after - either unset it (ephemeral key) unset MCP_JWT_SIGNING_KEY # or point at a real PEM file export MCP_JWT_SIGNING_KEY="file:///etc/litellm/keys/mcp_jwt.pem"
Defensive patterns
Strategy: validation
Validate before calling
import os
def check_mcp_jwt_signing_key() -> None:
val = os.environ.get("MCP_JWT_SIGNING_KEY")
if val is not None:
assert val.strip(), "MCP_JWT_SIGNING_KEY is set but empty - populate it or unset it (ephemeral key)"
material = val[len("file://"):] if val.startswith("file://") else val
if val.startswith("file://"):
import os.path
assert os.path.isfile(material) and os.path.getsize(material) > 0, f"key file missing/empty: {material}"
else:
assert "PRIVATE KEY" in val, "MCP_JWT_SIGNING_KEY does not look like a PEM private key"
check_mcp_jwt_signing_key() # run before starting the proxy Prevention
- Fail CI/deploy when a secrets-derived env var resolves to an empty string
- Prefer file:// paths mounted from a secret store over inline PEM values
- Remember: absent MCP_JWT_SIGNING_KEY means an ephemeral key is generated - explicit empty does not
When it happens
Trigger: MCP_JWT_SIGNING_KEY="" exported in the proxy environment - typically from a secrets loader writing an empty value (secret missing in the store), or a .env line like MCP_JWT_SIGNING_KEY= with no value.
Common situations: Docker/Kubernetes env populated from a missing or misnamed secret; CI pipelines where the key secret was never created; shell scripts exporting the var conditionally and leaving it empty; 'file://' pointing at an empty file is a sibling failure.
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
Related errors
- Missing JWT Public Key URL from environment.
- MCPJWTSigner guardrail requires a guardrail_name
- MCPJWTSigner: ttl_seconds must be > 0, got {resolved_ttl}
- Either 'REDIS_URL' or both 'REDIS_HOST' and 'REDIS_PORT' mus
- LEVOAI_ORG_ID environment variable is required for Levo inte
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/baa241aa9061653f.
Report an issue: GitHub.