{"record":{"id":"baa241aa9061653f","repo":"BerriAI/litellm","slug":"mcpjwtsigner-environment-variable-env-var-is","errorCode":null,"errorMessage":"MCPJWTSigner: environment variable '{env_var}' is set but empty.","messagePattern":"MCPJWTSigner: environment variable '(.+?)' is set but empty\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py","lineNumber":129,"sourceCode":"_mcp_jwt_signer_instance: Optional[\"MCPJWTSigner\"] = None\n\n_MCP_JWT_CALL_TYPES: Final = frozenset({\"call_mcp_tool\", \"list_mcp_tools\"})\n\n# Simple in-memory JWKS cache: keyed by JWKS URI → (keys_list, fetched_at).\n_jwks_cache: Final[dict[str, tuple[Sequence[Mapping[str, object]], float]]] = {}\n_JWKS_CACHE_TTL: Final = 3600  # 1 hour\n\n\ndef get_mcp_jwt_signer() -> Optional[\"MCPJWTSigner\"]:\n    \"\"\"Return the active MCPJWTSigner singleton, or None if not initialized.\"\"\"\n    return _mcp_jwt_signer_instance\n\n\ndef _load_private_key_from_env(env_var: str) -> RSAPrivateKey:\n    \"\"\"Load an RSA private key from an env var (PEM string or file:// path).\"\"\"\n    key_material: Final = os.environ.get(env_var, \"\")\n    if not key_material:\n        raise ValueError(f\"MCPJWTSigner: environment variable '{env_var}' is set but empty.\")\n    if key_material.startswith(\"file://\"):\n        path: Final = key_material[len(\"file://\") :]\n        with open(path, \"rb\") as f:\n            key_bytes = f.read()\n    else:\n        key_bytes = key_material.encode(\"utf-8\")\n    return serialization.load_pem_private_key(key_bytes, password=None)\n\n\ndef _generate_rsa_key_pair() -> RSAPrivateKey:\n    \"\"\"Generate a new RSA-2048 private key.\"\"\"\n    return rsa.generate_private_key(\n        public_exponent=65537,\n        key_size=2048,\n    )\n\n\ndef _int_to_base64url(n: int) -> str:","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py#L111-L147","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before - set but empty, raises ValueError at startup\nexport MCP_JWT_SIGNING_KEY=\"\"\n\n# after - either unset it (ephemeral key)\nunset MCP_JWT_SIGNING_KEY\n# or point at a real PEM file\nexport MCP_JWT_SIGNING_KEY=\"file:///etc/litellm/keys/mcp_jwt.pem\"","handlingStrategy":"validation","validationCode":"import os  \n  \ndef check_mcp_jwt_signing_key() -> None:  \n    val = os.environ.get(\"MCP_JWT_SIGNING_KEY\")  \n    if val is not None:  \n        assert val.strip(), \"MCP_JWT_SIGNING_KEY is set but empty - populate it or unset it (ephemeral key)\"  \n        material = val[len(\"file://\"):] if val.startswith(\"file://\") else val  \n        if val.startswith(\"file://\"):  \n            import os.path  \n            assert os.path.isfile(material) and os.path.getsize(material) > 0, f\"key file missing/empty: {material}\"  \n        else:  \n            assert \"PRIVATE KEY\" in val, \"MCP_JWT_SIGNING_KEY does not look like a PEM private key\"  \n  \ncheck_mcp_jwt_signing_key()  # run before starting the proxy","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["mcp","jwt","environment-variables","private-key","startup"],"backgroundTag":"missing-env-var","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}