Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
ce7bdb0
feat: Add global variable support for MCP server headers
Jan 14, 2026
3aca7de
test: Add comprehensive unit tests for IOKeyPairInputWithVariables co…
Jan 14, 2026
d672260
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 14, 2026
22a8de7
test: fix frontend Jest tests for IOKeyPairInputWithVariables component
Jan 14, 2026
d8e5748
fix: restore session_scope import to module level for test mocking
Jan 14, 2026
c62cc1c
chore: trigger CI rebuild to test for flaky Playwright test
Jan 14, 2026
feb0250
Update component index
Jan 14, 2026
19d6c02
chore: update Nvidia Remix starter project with session_scope import fix
Jan 14, 2026
f15b36a
perf(mcp): optimize global variable loading to prevent timeouts
Jan 14, 2026
5874625
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 14, 2026
fa01eac
fix(test): update Playwright test for new header input component stru…
Jan 14, 2026
00fc60f
chore: update Nvidia Remix starter project
Jan 14, 2026
3f0655e
Update component index
Jan 14, 2026
f06d4cf
fix: replace jose with jwt (#11285)
HimavarshaVS Jan 14, 2026
0b2fb1b
Update component index
Jan 14, 2026
464df1f
Merge upstream/main and resolve component_index.json conflict
Jan 14, 2026
31debfd
Update component index
Jan 14, 2026
c210d45
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 14, 2026
133d2ef
Merge latest upstream/main and resolve component_index.json conflict
Jan 14, 2026
a0b275c
Update component index
Jan 14, 2026
4d73ab6
chore: trigger CI rebuild
Jan 14, 2026
bf5934c
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 14, 2026
994dca6
refactor: remove langflow imports from lfx MCP component
Jan 15, 2026
a5a5935
fix: Handle global variables correctly for components
Jan 15, 2026
cb8e39a
Merge remote-tracking branch 'upstream/main' into feat/mcp-global-var…
Jan 15, 2026
7a65bea
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 15, 2026
7ea8133
Update component index
Jan 15, 2026
9aefdf0
Consider other failed decryption cases
Jan 15, 2026
116b516
fix(variable-service): Fix UUID conversion and type-based variable de…
Jan 15, 2026
2884d1c
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 15, 2026
b14b82b
Merge branch 'main' into feat/mcp-global-variable-headers
stevehaertel Jan 15, 2026
90b661b
chore: update starter project files
Jan 15, 2026
ba02c36
fix: remove explicit value assignment to allow credential redaction
Jan 15, 2026
56c1aa9
fix: handle credential redaction in frontend and fix Playwright test …
Jan 16, 2026
bb99a92
Merge branch 'main' into feat/mcp-global-variable-headers
stevehaertel Jan 16, 2026
38a0e65
Merge upstream/main into feat/mcp-global-variable-headers
Jan 16, 2026
f9bbe15
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 16, 2026
3f2c033
Update component index
Jan 16, 2026
03ae7ff
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 16, 2026
caa852d
fix: improve error handling and logging for variable decryption
Jan 20, 2026
a73b267
Update component index
Jan 20, 2026
3784164
Merge upstream/main into feat/mcp-global-variable-headers
Jan 20, 2026
923a142
Update component index
Jan 20, 2026
298fd93
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 20, 2026
3e01210
chore: trigger CI rebuild
Jan 20, 2026
70a7df8
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 20, 2026
3afe467
Updates to ensure backwards compatibility for encrypted generic varia…
jordanrfrazier Jan 20, 2026
fee2f16
Skip failed decryption
jordanrfrazier Jan 20, 2026
74f4f6d
Fix test
jordanrfrazier Jan 20, 2026
2c9404a
ruff
jordanrfrazier Jan 20, 2026
1c1087b
update starter projects
jordanrfrazier Jan 20, 2026
00b6b12
ruff
jordanrfrazier Jan 20, 2026
1dd0b01
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 20, 2026
5f8f561
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Jan 20, 2026
dad1ea3
comp index
jordanrfrazier Jan 20, 2026
0f10c96
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 20, 2026
adb52d8
remove unnecessary step in pandas series conversion
jordanrfrazier Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@
"filename": "src/backend/base/langflow/services/auth/utils.py",
"hashed_secret": "b894b81be94cf8fa8d7536475aaec876addf05c8",
"is_verified": false,
"line_number": 31,
"line_number": 32,
"is_secret": false
}
],
Expand Down Expand Up @@ -1528,5 +1528,5 @@
}
]
},
"generated_at": "2026-01-08T13:01:29Z"
"generated_at": "2026-01-15T17:59:11Z"
}
2 changes: 0 additions & 2 deletions scripts/check_deprecated_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,3 @@ def main() -> int:

if __name__ == "__main__":
sys.exit(main())

# Made with Bob
4 changes: 2 additions & 2 deletions src/backend/base/langflow/api/v1/knowledge_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ def calculate_text_metrics(df: pd.DataFrame, text_columns: list[str]) -> tuple[i
continue

text_series = df[col].astype(str).fillna("")
total_characters += int(text_series.str.len().sum().item())
total_words += int(text_series.str.split().str.len().sum().item())
total_characters += int(text_series.str.len().sum())
total_words += int(text_series.str.split().str.len().sum())

return total_words, total_characters

Expand Down
34 changes: 34 additions & 0 deletions src/backend/base/langflow/api/v2/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,45 @@ async def check_server(server_name: str) -> dict:
mcp_stdio_client = MCPStdioClient()
mcp_streamable_http_client = MCPStreamableHttpClient()
try:
# Get global variables from database for header resolution
request_variables = {}
try:
from sqlmodel import select

from langflow.services.auth import utils as auth_utils
from langflow.services.database.models.variable.model import Variable
from langflow.services.deps import get_settings_service

settings_service = get_settings_service()

# Load variables directly from database and decrypt ALL types (including CREDENTIAL)
stmt = select(Variable).where(Variable.user_id == current_user.id)
variables = list((await session.exec(stmt)).all())

# Decrypt variables based on type (following the pattern from get_all_decrypted_variables)
for variable in variables:
if variable.name and variable.value:
# Prior to v1.8, both Generic and Credential variables were encrypted.
# As such, must attempt to decrypt both types to ensure backwards-compatibility.
try:
decrypted_value = auth_utils.decrypt_api_key(
variable.value, settings_service=settings_service
)
request_variables[variable.name] = decrypted_value
except Exception as e: # noqa: BLE001
await logger.aerror(
f"Failed to decrypt credential variable '{variable.name}': {e}. "
"This credential will not be available for MCP server."
)
except Exception as e: # noqa: BLE001
await logger.awarning(f"Failed to load global variables for MCP server test: {e}")

mode, tool_list, _ = await update_tools(
server_name=server_name,
server_config=server_list["mcpServers"][server_name],
mcp_stdio_client=mcp_stdio_client,
mcp_streamable_http_client=mcp_streamable_http_client,
request_variables=request_variables,
)
server_info["mode"] = mode.lower()
server_info["toolsCount"] = len(tool_list)
Expand Down

Large diffs are not rendered by default.

28 changes: 13 additions & 15 deletions src/backend/base/langflow/services/auth/mcp_encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,11 @@ def encrypt_auth_settings(auth_settings: dict[str, Any] | None) -> dict[str, Any
try:
field_to_encrypt = encrypted_settings[field]
# Only encrypt if the value is not already encrypted
# Try to decrypt first - if it fails, it's not encrypted
try:
result = auth_utils.decrypt_api_key(field_to_encrypt, settings_service)
if not result:
msg = f"Failed to decrypt field {field}"
raise ValueError(msg)

# If decrypt succeeds, it's already encrypted
# Check if it's already encrypted using is_encrypted helper
if is_encrypted(field_to_encrypt):
logger.debug(f"Field {field} is already encrypted")
except (ValueError, TypeError, KeyError, InvalidToken):
# If decrypt fails, the value is plaintext and needs encryption
else:
# Not encrypted, encrypt it
encrypted_value = auth_utils.encrypt_api_key(field_to_encrypt, settings_service)
encrypted_settings[field] = encrypted_value
except (ValueError, TypeError, KeyError) as e:
Expand Down Expand Up @@ -111,10 +105,14 @@ def is_encrypted(value: str) -> bool:

settings_service = get_settings_service()
try:
# Try to decrypt - if it succeeds, it's encrypted
auth_utils.decrypt_api_key(value, settings_service)
# Try to decrypt - if it succeeds and returns a different value, it's encrypted
decrypted = auth_utils.decrypt_api_key(value, settings_service)
# If decryption returns empty string, it's encrypted with wrong key
if not decrypted:
return True
# If it returns a different value, it's successfully decrypted (was encrypted)
# If it returns the same value, something unexpected happened
return decrypted != value # noqa: TRY300
except (ValueError, TypeError, KeyError, InvalidToken):
# If decryption fails, it's not encrypted
return False
else:
# If decryption fails with exception, assume it's encrypted but can't be decrypted
return True
36 changes: 28 additions & 8 deletions src/backend/base/langflow/services/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,17 +622,20 @@ def encrypt_api_key(api_key: str, settings_service: SettingsService):
def decrypt_api_key(encrypted_api_key: str, settings_service: SettingsService):
"""Decrypt the provided encrypted API key using Fernet decryption.

This function first attempts to decrypt the API key by encoding it,
assuming it is a properly encoded string. If that fails, it logs a detailed
debug message including the exception information and retries decryption
using the original string input.
This function supports both encrypted and plain text values. It first attempts
to decrypt the API key by encoding it, assuming it is a properly encrypted string.
If that fails, it retries decryption using the original string input. If both
decryption attempts fail, it checks if the value looks like a Fernet token
(starts with "gAAAAA"). If it does, it's likely encrypted with a different key
and returns empty string. Otherwise, it assumes plain text and returns as-is.

Args:
encrypted_api_key (str): The encrypted API key.
encrypted_api_key (str): The encrypted API key or plain text value.
settings_service (SettingsService): Service providing authentication settings.

Returns:
str: The decrypted API key, or an empty string if decryption cannot be performed.
str: The decrypted API key, the original value if plain text, or empty string
if it's encrypted with a different key.
"""
fernet = get_fernet(settings_service)
if isinstance(encrypted_api_key, str):
Expand All @@ -644,8 +647,25 @@ def decrypt_api_key(encrypted_api_key: str, settings_service: SettingsService):
"Retrying decryption using the raw string input.",
primary_exception,
)
return fernet.decrypt(encrypted_api_key).decode()
return ""
try:
return fernet.decrypt(encrypted_api_key).decode()
except Exception as secondary_exception: # noqa: BLE001
# Check if this looks like a Fernet token (base64 encoded, starts with gAAAAA)
if encrypted_api_key.startswith("gAAAAA"):
logger.warning(
"Failed to decrypt stored value (likely encrypted with different key). "
"Error: %s. Returning empty string.",
secondary_exception,
)
return ""
# Assume the value is plain text and return it as-is
logger.debug(
"Value does not appear to be encrypted (no Fernet token signature). Returning value as plain text."
)
return encrypted_api_key

msg = "Unexpected variable type. Expected string"
raise ValueError(msg)


# MCP-specific authentication functions that always behave as if skip_auth_auto_login is True
Expand Down
81 changes: 68 additions & 13 deletions src/backend/base/langflow/services/variable/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
from datetime import datetime, timezone
from typing import TYPE_CHECKING
from uuid import UUID

from lfx.log.logger import logger
from sqlmodel import select
Expand All @@ -15,7 +16,6 @@

if TYPE_CHECKING:
from collections.abc import Sequence
from uuid import UUID

from lfx.services.settings.service import SettingsService
from sqlmodel.ext.asyncio.session import AsyncSession
Expand Down Expand Up @@ -176,30 +176,68 @@ async def get_variable(
)
raise TypeError(msg)

# we decrypt the value
return auth_utils.decrypt_api_key(variable.value, settings_service=self.settings_service)
# Only decrypt CREDENTIAL type variables; GENERIC variables are stored as plain text
if variable.type == CREDENTIAL_TYPE:
return auth_utils.decrypt_api_key(variable.value, settings_service=self.settings_service)
# GENERIC type - return as-is
return variable.value

async def get_all(self, user_id: UUID | str, session: AsyncSession) -> list[VariableRead]:
stmt = select(Variable).where(Variable.user_id == user_id)
variables = list((await session.exec(stmt)).all())
# For variables of type 'Generic', attempt to decrypt the value.
# If decryption fails, assume the value is already plaintext.
variables_read = []
for variable in variables:
value = None
if variable.type == GENERIC_TYPE:
try:
value = auth_utils.decrypt_api_key(variable.value, settings_service=self.settings_service)
except Exception as e: # noqa: BLE001
await logger.adebug(
f"Decryption of {variable.type} failed for variable '{variable.name}': {e}. Assuming plaintext."
)
value = variable.value
value = auth_utils.decrypt_api_key(variable.value, settings_service=self.settings_service)
if not value:
# If decryption fails (likely due to encryption by different key), skip this variable
continue

# Model validate will set value to None if credential type
variable_read = VariableRead.model_validate(variable, from_attributes=True)
variable_read.value = value
if variable.type == GENERIC_TYPE:
variable_read.value = value

variables_read.append(variable_read)
return variables_read

async def get_all_decrypted_variables(
self,
user_id: UUID | str,
session: AsyncSession,
) -> dict[str, str]:
"""Get all variables for a user with decrypted values.

Args:
user_id: The user ID to get variables for
session: Database session

Returns:
Dictionary mapping variable names to decrypted values
"""
# Convert string to UUID if needed for SQLAlchemy query
user_id_uuid = UUID(user_id) if isinstance(user_id, str) else user_id
stmt = select(Variable).where(Variable.user_id == user_id_uuid)
variables = (await session.exec(stmt)).all()

result = {}
for var in variables:
if var.name and var.value:
try:
decrypted_value = auth_utils.decrypt_api_key(var.value, settings_service=self.settings_service)
except Exception as e: # noqa: BLE001
await logger.awarning(f"Decryption failed for variable '{var.name}': {e}. Skipping")
continue

if not decrypted_value:
await logger.awarning(f"Decryption returned empty for variable '{var.name}'. Skipping")
continue

result[var.name] = decrypted_value

return result

async def get_variable_by_id(
self,
user_id: UUID | str,
Expand Down Expand Up @@ -229,6 +267,15 @@ async def update_variable(
if not variable:
msg = f"{name} variable not found."
raise ValueError(msg)

# Validate that GENERIC variables don't start with Fernet signature
if variable.type == GENERIC_TYPE and value.startswith("gAAAAA"):
msg = (
f"Generic variable '{name}' cannot start with 'gAAAAA' as this is reserved "
"for encrypted values. Please use a different value."
)
raise ValueError(msg)

# Only encrypt CREDENTIAL_TYPE variables
if variable.type == CREDENTIAL_TYPE:
variable.value = auth_utils.encrypt_api_key(value, settings_service=self.settings_service)
Expand Down Expand Up @@ -302,6 +349,14 @@ async def create_variable(
type_: str = CREDENTIAL_TYPE,
session: AsyncSession,
):
# Validate that GENERIC variables don't start with Fernet signature
if type_ == GENERIC_TYPE and value.startswith("gAAAAA"):
msg = (
f"Generic variable '{name}' cannot start with 'gAAAAA' as this is reserved "
"for encrypted values. Please use a different value."
)
raise ValueError(msg)

# Only encrypt CREDENTIAL_TYPE variables
encrypted_value = (
auth_utils.encrypt_api_key(value, settings_service=self.settings_service)
Expand Down
2 changes: 0 additions & 2 deletions src/backend/base/langflow/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
"""Tests package for langflow."""

# Made with Bob
2 changes: 0 additions & 2 deletions src/backend/base/langflow/tests/services/database/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
"""Database tests package."""

# Made with Bob
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
"""Database models tests package."""

# Made with Bob
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
"""Transaction models tests package."""

# Made with Bob
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,3 @@ async def test_code_key_not_saved_to_database():
assert "code" not in serialized_inputs
assert "param1" in serialized_inputs
assert "param2" in serialized_inputs


# Made with Bob
Loading
Loading