feat: add servers persistence to MCP connection component, add MCP connections settings page#8388
Conversation
… 0.4.3 respectively
Switching the provider in the Agent component sometimes left models from the previous provider visible/selected. We now filter against the new , ensuring only models that belong to the active provider remain. * src/frontend/src/components/core/dropdownComponent/index.tsx – add guard when rebuilding * tests/extended/regression/general-bugs-dropdown-select-not-in-list.spec.ts – expand coverage for “model not in list” edge-cases Co-authored-by: Cristian Lousa <cristian.lousa@gmail.com>
…ean variables when switching tabs
…_persistence`) Here’s a faster implementation, with reduced function calls and optimized list processing. Key changes. - Combine checks in `_process_headers`, avoid function call for `_is_valid_key_value_item` inside loop: inline the check. - Eliminate superfluous try/except: pre-check for dict type and key presence. - Dictionary construction is done with a generator expression and a one-liner dict constructor for speed. All original comments preserved except where the code was changed. This version avoids extra function call overhead, skips needless try/except, and is generally the fastest idiomatic way in Python for this purpose. Return values are identical.
| processed_headers = {} | ||
| try: | ||
| for item in headers: | ||
| if not _is_valid_key_value_item(item): | ||
| continue | ||
| key = item["key"] | ||
| value = item["value"] | ||
| processed_headers[key] = value | ||
| except (KeyError, TypeError, ValueError): | ||
| return {} # Return empty dictionary instead of None | ||
| return processed_headers |
There was a problem hiding this comment.
⚡️Codeflash found 34% (0.34x) speedup for _process_headers in src/backend/base/langflow/base/mcp/util.py
⏱️ Runtime : 1.22 milliseconds → 910 microseconds (best of 111 runs)
📝 Explanation and details
Here’s a faster implementation, with reduced function calls and optimized list processing. Key changes.- Combine checks in
_process_headers, avoid function call for_is_valid_key_value_iteminside loop: inline the check. - Eliminate superfluous try/except: pre-check for dict type and key presence.
- Dictionary construction is done with a generator expression and a one-liner dict constructor for speed.
All original comments preserved except where the code was changed.
This version avoids extra function call overhead, skips needless try/except, and is generally the fastest idiomatic way in Python for this purpose.
Return values are identical.
✅ Correctness verification report:
| Test | Status |
|---|---|
| ⚙️ Existing Unit Tests | 🔘 None Found |
| 🌀 Generated Regression Tests | ✅ 47 Passed |
| ⏪ Replay Tests | 🔘 None Found |
| 🔎 Concolic Coverage Tests | 🔘 None Found |
| 📊 Tests Coverage | 88.9% |
🌀 Generated Regression Tests Details
from typing import Any
# imports
import pytest # used for our unit tests
from langflow.base.mcp.util import _process_headers
# unit tests
# -------------------- BASIC TEST CASES --------------------
def test_none_input_returns_empty_dict():
# None should return an empty dict
codeflash_output = _process_headers(None)
def test_empty_dict_returns_empty_dict():
# An empty dict should return itself
codeflash_output = _process_headers({})
def test_simple_dict_returns_same_dict():
# A simple dict should return itself
headers = {"Content-Type": "application/json", "Accept": "text/html"}
codeflash_output = _process_headers(headers)
def test_list_of_key_value_dicts():
# A list of dicts with 'key' and 'value' should be converted to dict
headers = [
{"key": "Content-Type", "value": "application/json"},
{"key": "Accept", "value": "text/html"}
]
expected = {"Content-Type": "application/json", "Accept": "text/html"}
codeflash_output = _process_headers(headers)
def test_list_with_duplicate_keys():
# Later values should overwrite earlier ones for the same key
headers = [
{"key": "A", "value": "1"},
{"key": "A", "value": "2"}
]
expected = {"A": "2"}
codeflash_output = _process_headers(headers)
def test_list_with_non_dict_items():
# Non-dict items should be ignored
headers = [
{"key": "A", "value": "1"},
"not a dict",
123,
None,
{"key": "B", "value": "2"}
]
expected = {"A": "1", "B": "2"}
codeflash_output = _process_headers(headers)
def test_list_with_missing_key_or_value():
# Items missing 'key' or 'value' should be ignored
headers = [
{"key": "A", "value": "1"},
{"key": "B"}, # missing value
{"value": "2"}, # missing key
{}, # missing both
{"key": "C", "value": "3"}
]
expected = {"A": "1", "C": "3"}
codeflash_output = _process_headers(headers)
def test_empty_list_returns_empty_dict():
# An empty list should return an empty dict
codeflash_output = _process_headers([])
# -------------------- EDGE TEST CASES --------------------
def test_list_with_key_value_none():
# None as key or value should be accepted as a valid key/value
headers = [
{"key": None, "value": "something"},
{"key": "foo", "value": None}
]
expected = {None: "something", "foo": None}
codeflash_output = _process_headers(headers)
def test_list_with_extra_keys_in_dict():
# Extra keys in the item dict should be ignored
headers = [
{"key": "A", "value": "1", "extra": "ignored"},
{"key": "B", "value": "2", "foo": "bar"}
]
expected = {"A": "1", "B": "2"}
codeflash_output = _process_headers(headers)
def test_list_with_nested_dict_as_value():
# Values can be any object, including dicts
headers = [
{"key": "A", "value": {"foo": "bar"}},
{"key": "B", "value": [1, 2, 3]}
]
expected = {"A": {"foo": "bar"}, "B": [1, 2, 3]}
codeflash_output = _process_headers(headers)
def test_dict_with_non_string_keys():
# Dicts with non-string keys should be returned as is
headers = {1: "a", (2, 3): "b"}
codeflash_output = _process_headers(headers)
def test_list_with_items_that_raise_exceptions():
# If item access raises KeyError/TypeError/ValueError, should skip or return {}
class BadItem:
def __getitem__(self, key):
raise KeyError("bad key")
headers = [
{"key": "A", "value": "1"},
BadItem(),
{"key": "B", "value": "2"}
]
# Because exception is caught, function should return {}
codeflash_output = _process_headers([BadItem()])
def test_string_input_returns_empty_dict():
# Strings are not valid inputs, should return {}
codeflash_output = _process_headers("Content-Type: application/json")
def test_integer_input_returns_empty_dict():
# Integers are not valid, should return {}
codeflash_output = _process_headers(42)
def test_list_with_all_invalid_items_returns_empty_dict():
# If all items are invalid, should return empty dict
headers = ["not a dict", 123, None, []]
codeflash_output = _process_headers(headers)
def test_list_with_dict_missing_both_key_and_value():
# Dicts missing both 'key' and 'value' should be ignored
headers = [{"foo": "bar"}, {"baz": "qux"}]
codeflash_output = _process_headers(headers)
def test_list_with_items_of_various_types():
# List with a mix of valid and invalid items
headers = [
{"key": "A", "value": "1"},
99,
{"key": "B", "value": "2"},
[],
{"foo": "bar"},
{"key": "C", "value": "3"}
]
expected = {"A": "1", "B": "2", "C": "3"}
codeflash_output = _process_headers(headers)
# -------------------- LARGE SCALE TEST CASES --------------------
def test_large_dict_input():
# Test with a large dictionary (up to 1000 items)
headers = {f"Header-{i}": f"Value-{i}" for i in range(1000)}
codeflash_output = _process_headers(headers)
def test_large_list_of_key_value_dicts():
# Test with a large list of key-value dicts (up to 1000 items)
headers = [{"key": f"Header-{i}", "value": f"Value-{i}"} for i in range(1000)]
expected = {f"Header-{i}": f"Value-{i}" for i in range(1000)}
codeflash_output = _process_headers(headers)
def test_large_list_with_some_invalid_items():
# Large list with some invalid items interspersed
headers = []
for i in range(500):
headers.append({"key": f"Header-{i}", "value": f"Value-{i}"})
headers.append(123) # invalid
headers.append({"foo": "bar"}) # invalid
expected = {f"Header-{i}": f"Value-{i}" for i in range(500)}
codeflash_output = _process_headers(headers)
def test_large_list_with_duplicate_keys():
# Later values should overwrite earlier ones for duplicate keys
headers = []
for i in range(500):
headers.append({"key": "X", "value": str(i)})
# Only the last value should remain
codeflash_output = _process_headers(headers)
def test_large_list_all_invalid_items():
# Large list with all invalid items should return empty dict
headers = [123, None, "foo", [], {}] * 200
codeflash_output = _process_headers(headers)
def test_large_list_with_nested_values():
# Large list with complex nested values
headers = [{"key": f"K{i}", "value": {"nested": [i, i*2]}} for i in range(1000)]
expected = {f"K{i}": {"nested": [i, i*2]} for i in range(1000)}
codeflash_output = _process_headers(headers)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from typing import Any
# imports
import pytest # used for our unit tests
from langflow.base.mcp.util import _process_headers
# unit tests
# ------------------- BASIC TEST CASES -------------------
def test_none_returns_empty_dict():
"""Test that None input returns empty dict."""
codeflash_output = _process_headers(None)
def test_dict_input_returns_same_dict():
"""Test that a dict input is returned as is."""
d = {"A": "B", "C": "D"}
codeflash_output = _process_headers(d) # Should be the same object
def test_empty_dict_input():
"""Test that an empty dict input returns the same empty dict."""
d = {}
codeflash_output = _process_headers(d)
def test_list_of_valid_key_value_dicts():
"""Test a list of valid {'key':..., 'value':...} dicts."""
headers = [{"key": "A", "value": "B"}, {"key": "C", "value": "D"}]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_list_with_duplicate_keys():
"""Test that later duplicate keys overwrite earlier ones."""
headers = [
{"key": "A", "value": "B"},
{"key": "A", "value": "C"}
]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_list_with_mixed_valid_and_invalid_items():
"""Test that invalid items in a list are skipped."""
headers = [
{"key": "A", "value": "B"},
{"foo": "bar"}, # invalid
{"key": "C", "value": "D"},
123, # invalid
None, # invalid
{"key": "E"} # missing value
]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_empty_list_returns_empty_dict():
"""Test that an empty list returns an empty dict."""
codeflash_output = _process_headers([])
# ------------------- EDGE TEST CASES -------------------
def test_list_with_only_invalid_items():
"""Test that a list with only invalid items returns empty dict."""
headers = [123, None, "foo", {"foo": "bar"}, {"key": "A"}, {"value": "B"}]
codeflash_output = _process_headers(headers)
def test_list_with_key_value_none():
"""Test that None as key or value is accepted and included."""
headers = [
{"key": None, "value": "foo"},
{"key": "bar", "value": None}
]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_list_with_empty_strings_as_keys_and_values():
"""Test that empty string keys and values are accepted."""
headers = [
{"key": "", "value": ""},
{"key": "A", "value": ""}
]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_list_with_non_string_keys_and_values():
"""Test that non-string keys/values are accepted as dictionary keys/values."""
headers = [
{"key": 1, "value": 2},
{"key": (3, 4), "value": [5, 6]}
]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_list_with_nested_dicts_as_values():
"""Test that nested dicts as values are accepted."""
headers = [
{"key": "A", "value": {"nested": 1}},
{"key": "B", "value": [1, 2, 3]}
]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_string_input_returns_empty_dict():
"""Test that a string input returns empty dict."""
codeflash_output = _process_headers("foo")
def test_integer_input_returns_empty_dict():
"""Test that an integer input returns empty dict."""
codeflash_output = _process_headers(12345)
def test_tuple_input_returns_empty_dict():
"""Test that a tuple input returns empty dict."""
codeflash_output = _process_headers((1, 2))
def test_list_with_dicts_missing_key_or_value():
"""Test that dicts missing 'key' or 'value' are skipped."""
headers = [
{"key": "A"}, # missing value
{"value": "B"}, # missing key
{"key": "C", "value": "D"}
]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_list_with_dicts_with_extra_keys():
"""Test that dicts with extra keys are still processed."""
headers = [
{"key": "A", "value": "B", "foo": "bar"},
{"key": "C", "value": "D", "baz": 123}
]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_list_with_key_and_value_as_same_string():
"""Test that keys and values can be the same string."""
headers = [
{"key": "A", "value": "A"},
{"key": "B", "value": "B"}
]
codeflash_output = _process_headers(headers); result = codeflash_output
# ------------------- LARGE SCALE TEST CASES -------------------
def test_large_list_of_valid_headers():
"""Test with a large list of valid headers."""
headers = [{"key": f"key{i}", "value": f"value{i}"} for i in range(1000)]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_large_list_with_some_invalid_items():
"""Test with a large list containing some invalid items."""
headers = [{"key": f"key{i}", "value": f"value{i}"} for i in range(500)]
# Insert invalid items at every 50th position
for i in range(0, 500, 50):
headers.insert(i, 123)
headers.insert(i+1, {"foo": "bar"})
codeflash_output = _process_headers(headers); result = codeflash_output
def test_large_dict_input():
"""Test passing a large dict directly."""
d = {f"k{i}": f"v{i}" for i in range(1000)}
codeflash_output = _process_headers(d); result = codeflash_output
def test_large_list_with_duplicate_keys():
"""Test that last occurrence of duplicate keys is kept in large list."""
headers = [{"key": f"dup", "value": i} for i in range(1000)]
codeflash_output = _process_headers(headers); result = codeflash_output
def test_large_list_with_all_invalid_items():
"""Test that a large list of all invalid items returns empty dict."""
headers = [123 for _ in range(1000)]
codeflash_output = _process_headers(headers)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.To test or edit this optimization locally git merge codeflash/optimize-pr8388-2025-06-10T22.01.35
| processed_headers = {} | |
| try: | |
| for item in headers: | |
| if not _is_valid_key_value_item(item): | |
| continue | |
| key = item["key"] | |
| value = item["value"] | |
| processed_headers[key] = value | |
| except (KeyError, TypeError, ValueError): | |
| return {} # Return empty dictionary instead of None | |
| return processed_headers | |
| # Fastest: build dict in one pass, skipping bad items without exceptions | |
| return { | |
| item["key"]: item["value"] | |
| for item in headers | |
| if isinstance(item, dict) and "key" in item and "value" in item | |
| } |
| try: | ||
| for item in headers: | ||
| if not _is_valid_key_value_item(item): | ||
| continue | ||
| key = item["key"] | ||
| value = item["value"] | ||
| processed_headers[key] = value | ||
| except (KeyError, TypeError, ValueError): |
There was a problem hiding this comment.
⚡️Codeflash found 32% (0.32x) speedup for _process_headers in src/backend/base/langflow/base/mcp/util.py
⏱️ Runtime : 940 microseconds → 711 microseconds (best of 76 runs)
📝 Explanation and details
Here is the optimized version of your code, preserving all function signatures and functionality, but avoiding repeated dictionary checks and function calls in tight loops. The main optimizations.- Inline the content of
_is_valid_key_value_itemin_process_headersto save on function call overhead in large loops. - Reduce dictionary lookups: Instead of
"key" in item and "value" in item", try to extract "key" and "value" first and catchKeyError` for invalid dicts. - Use type local variables (common in micro-optimizations) to avoid repeated type lookups.
Here’s the optimized code.
Key changes/optimizations:
- The
isinstance(item, dict)and then direct key-extraction-and-exception short-circuits many unnecessary"key" in itemand"value" in item"lookups. - Catches only the needed exceptions at the narrowest possible scope.
- Kept reference to
dictlocal in the loop (dict_type) as a micro-optimization, though in current versions of CPython this is less important, but included for completeness.
This version will be faster, especially in the tight list iteration path.
✅ Correctness verification report:
| Test | Status |
|---|---|
| ⚙️ Existing Unit Tests | 🔘 None Found |
| 🌀 Generated Regression Tests | ✅ 57 Passed |
| ⏪ Replay Tests | 🔘 None Found |
| 🔎 Concolic Coverage Tests | 🔘 None Found |
| 📊 Tests Coverage | 100.0% |
🌀 Generated Regression Tests Details
from typing import Any
# imports
import pytest # used for our unit tests
from langflow.base.mcp.util import _process_headers
# unit tests
# ----------------------- BASIC TEST CASES -----------------------
def test_none_input_returns_empty_dict():
"""Test that None input returns an empty dictionary."""
codeflash_output = _process_headers(None)
def test_empty_dict_input_returns_same():
"""Test that an empty dict returns itself."""
codeflash_output = _process_headers({})
def test_simple_dict_input_returns_same():
"""Test that a simple dict is returned as is."""
d = {"Content-Type": "application/json"}
codeflash_output = _process_headers(d) # Should be the same object
def test_list_of_valid_key_value_dicts():
"""Test list of valid key-value dicts are converted properly."""
headers = [
{"key": "Accept", "value": "application/json"},
{"key": "User-Agent", "value": "pytest"}
]
expected = {"Accept": "application/json", "User-Agent": "pytest"}
codeflash_output = _process_headers(headers)
def test_list_with_duplicate_keys_last_wins():
"""Test that duplicate keys in list, last one wins."""
headers = [
{"key": "Accept", "value": "application/xml"},
{"key": "Accept", "value": "application/json"}
]
expected = {"Accept": "application/json"}
codeflash_output = _process_headers(headers)
def test_list_with_non_dict_items_are_ignored():
"""Test that non-dict items in the list are ignored."""
headers = [
{"key": "A", "value": "1"},
42,
"foo",
None,
{"key": "B", "value": "2"}
]
expected = {"A": "1", "B": "2"}
codeflash_output = _process_headers(headers)
def test_list_with_missing_key_or_value_are_ignored():
"""Test that dicts missing 'key' or 'value' are ignored."""
headers = [
{"key": "A", "value": "1"},
{"value": "2"},
{"key": "B"},
{},
{"key": "C", "value": "3"}
]
expected = {"A": "1", "C": "3"}
codeflash_output = _process_headers(headers)
def test_list_of_empty_dicts_returns_empty():
"""Test that a list of empty dicts returns empty dict."""
headers = [{}, {}, {}]
codeflash_output = _process_headers(headers)
def test_empty_list_returns_empty_dict():
"""Test that an empty list returns empty dict."""
codeflash_output = _process_headers([])
def test_str_input_returns_empty_dict():
"""Test that a string input returns empty dict."""
codeflash_output = _process_headers("Content-Type: application/json")
def test_int_input_returns_empty_dict():
"""Test that an int input returns empty dict."""
codeflash_output = _process_headers(123)
def test_float_input_returns_empty_dict():
"""Test that a float input returns empty dict."""
codeflash_output = _process_headers(3.14)
# ----------------------- EDGE TEST CASES -----------------------
def test_dict_with_non_string_keys_and_values():
"""Test dict input with non-string keys/values is returned as is."""
d = {42: [1,2,3], (1,2): None}
codeflash_output = _process_headers(d)
def test_list_with_key_value_none():
"""Test list with 'key' or 'value' as None."""
headers = [
{"key": None, "value": "something"},
{"key": "X", "value": None}
]
expected = {None: "something", "X": None}
codeflash_output = _process_headers(headers)
def test_list_with_key_value_empty_string():
"""Test list with empty string as key or value."""
headers = [
{"key": "", "value": "foo"},
{"key": "bar", "value": ""}
]
expected = {"": "foo", "bar": ""}
codeflash_output = _process_headers(headers)
def test_list_with_extra_keys_ignored():
"""Test dicts in list with extra keys are still processed."""
headers = [
{"key": "A", "value": "1", "other": "x"},
{"key": "B", "value": "2", "foo": "bar"}
]
expected = {"A": "1", "B": "2"}
codeflash_output = _process_headers(headers)
def test_list_with_nested_dicts_as_value():
"""Test dicts with nested dict as value."""
headers = [
{"key": "A", "value": {"nested": 1}},
{"key": "B", "value": [1,2,3]}
]
expected = {"A": {"nested": 1}, "B": [1,2,3]}
codeflash_output = _process_headers(headers)
def test_list_with_all_invalid_items_returns_empty():
"""Test list where all items are invalid returns empty dict."""
headers = [
42,
"foo",
None,
{"foo": "bar"},
[]
]
codeflash_output = _process_headers(headers)
def test_list_with_dicts_missing_both_key_and_value():
"""Test list where dicts are missing both 'key' and 'value'."""
headers = [
{"foo": "bar"},
{"baz": 1}
]
codeflash_output = _process_headers(headers)
def test_list_with_key_value_as_tuple():
"""Test list with tuple as key or value."""
headers = [
{"key": ("a", "b"), "value": 1},
{"key": "c", "value": (2, 3)}
]
expected = {("a", "b"): 1, "c": (2, 3)}
codeflash_output = _process_headers(headers)
def test_list_with_dicts_with_boolean_keys():
"""Test list with boolean keys."""
headers = [
{"key": True, "value": "yes"},
{"key": False, "value": "no"}
]
expected = {True: "yes", False: "no"}
codeflash_output = _process_headers(headers)
def test_list_with_dicts_with_integer_keys():
"""Test list with integer keys."""
headers = [
{"key": 1, "value": "one"},
{"key": 2, "value": "two"}
]
expected = {1: "one", 2: "two"}
codeflash_output = _process_headers(headers)
def test_list_with_dicts_with_float_keys():
"""Test list with float keys."""
headers = [
{"key": 1.1, "value": "one.one"},
{"key": 2.2, "value": "two.two"}
]
expected = {1.1: "one.one", 2.2: "two.two"}
codeflash_output = _process_headers(headers)
def test_list_with_dicts_with_falsy_keys():
"""Test list with falsy keys like 0, '', False."""
headers = [
{"key": 0, "value": "zero"},
{"key": "", "value": "empty"},
{"key": False, "value": "false"}
]
expected = {0: "zero", "": "empty", False: "false"}
codeflash_output = _process_headers(headers)
# ----------------------- LARGE SCALE TEST CASES -----------------------
def test_large_list_of_valid_headers():
"""Test processing a large list of valid header dicts."""
headers = [{"key": f"Key{i}", "value": f"Value{i}"} for i in range(500)]
expected = {f"Key{i}": f"Value{i}" for i in range(500)}
codeflash_output = _process_headers(headers)
def test_large_list_with_some_invalid_items():
"""Test large list with some invalid items scattered throughout."""
headers = []
expected = {}
for i in range(500):
headers.append({"key": f"K{i}", "value": f"V{i}"})
expected[f"K{i}"] = f"V{i}"
if i % 10 == 0:
headers.append(42)
headers.append({"foo": "bar"})
headers.append(None)
codeflash_output = _process_headers(headers)
def test_large_list_with_duplicate_keys():
"""Test large list with duplicate keys, ensure last wins."""
headers = []
for i in range(250):
headers.append({"key": "DUP", "value": f"V{i}"})
expected = {"DUP": f"V{249}"}
codeflash_output = _process_headers(headers)
def test_large_dict_input_returns_same():
"""Test that a large dict input is returned as is."""
d = {f"K{i}": f"V{i}" for i in range(500)}
codeflash_output = _process_headers(d)
def test_large_list_with_all_invalid_items():
"""Test large list with all invalid items returns empty dict."""
headers = [42, None, "foo", {"foo": "bar"}] * 250
codeflash_output = _process_headers(headers)
from typing import Any
# imports
import pytest # used for our unit tests
from langflow.base.mcp.util import _process_headers
# unit tests
# =========================
# 1. Basic Test Cases
# =========================
def test_none_input_returns_empty_dict():
"""Test that None input returns an empty dictionary."""
codeflash_output = _process_headers(None)
def test_empty_dict_input_returns_empty_dict():
"""Test that an empty dict input returns an empty dict."""
codeflash_output = _process_headers({})
def test_dict_input_returns_same_dict():
"""Test that a dictionary input is returned as is."""
d = {"A": "B", "C": "D"}
codeflash_output = _process_headers(d)
def test_list_of_valid_key_value_dicts():
"""Test that a list of valid key-value dicts is converted properly."""
headers = [{"key": "A", "value": "B"}, {"key": "C", "value": "D"}]
expected = {"A": "B", "C": "D"}
codeflash_output = _process_headers(headers)
def test_list_of_valid_key_value_dicts_with_duplicate_keys():
"""Test that later duplicate keys overwrite earlier ones."""
headers = [
{"key": "A", "value": "B"},
{"key": "A", "value": "C"},
{"key": "D", "value": "E"}
]
expected = {"A": "C", "D": "E"}
codeflash_output = _process_headers(headers)
# =========================
# 2. Edge Test Cases
# =========================
def test_list_with_invalid_items_skips_them():
"""Test that invalid items in the list are skipped."""
headers = [
{"key": "A", "value": "B"},
{"foo": "bar"}, # invalid
123, # invalid
{"key": "C", "value": "D"}
]
expected = {"A": "B", "C": "D"}
codeflash_output = _process_headers(headers)
def test_list_with_empty_dicts_skips_them():
"""Test that empty dicts in the list are skipped."""
headers = [
{},
{"key": "X", "value": "Y"},
{}
]
expected = {"X": "Y"}
codeflash_output = _process_headers(headers)
def test_list_with_missing_key_field_skips_item():
"""Test that dicts missing 'key' are skipped."""
headers = [
{"value": "A"},
{"key": "X", "value": "Y"}
]
expected = {"X": "Y"}
codeflash_output = _process_headers(headers)
def test_list_with_missing_value_field_skips_item():
"""Test that dicts missing 'value' are skipped."""
headers = [
{"key": "A"},
{"key": "X", "value": "Y"}
]
expected = {"X": "Y"}
codeflash_output = _process_headers(headers)
def test_list_with_non_dict_items_skips_them():
"""Test that non-dict items in the list are skipped."""
headers = [
{"key": "A", "value": "B"},
"not a dict",
["key", "value"],
None,
{"key": "C", "value": "D"}
]
expected = {"A": "B", "C": "D"}
codeflash_output = _process_headers(headers)
def test_string_input_returns_empty_dict():
"""Test that a string input returns an empty dict."""
codeflash_output = _process_headers("Content-Type: application/json")
def test_integer_input_returns_empty_dict():
"""Test that an integer input returns an empty dict."""
codeflash_output = _process_headers(12345)
def test_list_with_all_invalid_items_returns_empty_dict():
"""Test that a list with all invalid items returns an empty dict."""
headers = [123, "foo", None, [], {}]
codeflash_output = _process_headers(headers)
def test_list_with_dicts_missing_both_key_and_value():
"""Test that dicts missing both 'key' and 'value' are skipped."""
headers = [
{"foo": "bar"},
{"baz": "qux"}
]
codeflash_output = _process_headers(headers)
def test_list_with_key_value_none():
"""Test that 'key' or 'value' fields can be None and are included."""
headers = [
{"key": None, "value": "A"},
{"key": "B", "value": None}
]
expected = {None: "A", "B": None}
codeflash_output = _process_headers(headers)
def test_list_with_key_value_empty_string():
"""Test that empty string keys and values are handled."""
headers = [
{"key": "", "value": "A"},
{"key": "B", "value": ""}
]
expected = {"": "A", "B": ""}
codeflash_output = _process_headers(headers)
def test_list_with_key_value_various_types():
"""Test that keys and values of various types are handled."""
headers = [
{"key": 1, "value": 2},
{"key": (3, 4), "value": [5, 6]}
]
expected = {1: 2, (3, 4): [5, 6]}
codeflash_output = _process_headers(headers)
def test_list_with_nested_dict_as_value():
"""Test that values can be dicts."""
headers = [
{"key": "A", "value": {"foo": "bar"}}
]
expected = {"A": {"foo": "bar"}}
codeflash_output = _process_headers(headers)
def test_list_with_nested_dict_as_key():
"""Test that dict keys are allowed (though not recommended)."""
headers = [
{"key": frozenset({"foo": "bar"}.items()), "value": "baz"}
]
expected = {frozenset({"foo": "bar"}.items()): "baz"}
codeflash_output = _process_headers(headers)
def test_list_with_mixed_valid_and_invalid_items():
"""Test that only valid items are included in the result."""
headers = [
{"key": "A", "value": "B"},
{"foo": "bar"},
{"key": "C"},
{"key": "D", "value": "E"}
]
expected = {"A": "B", "D": "E"}
codeflash_output = _process_headers(headers)
def test_list_with_value_is_list():
"""Test that values can be lists."""
headers = [
{"key": "A", "value": [1, 2, 3]}
]
expected = {"A": [1, 2, 3]}
codeflash_output = _process_headers(headers)
# =========================
# 3. Large Scale Test Cases
# =========================
def test_large_dict_input():
"""Test that a large dictionary is returned as is."""
d = {str(i): i for i in range(1000)}
codeflash_output = _process_headers(d)
def test_large_list_of_valid_key_value_dicts():
"""Test processing of a large list of valid key-value dicts."""
headers = [{"key": str(i), "value": i} for i in range(1000)]
expected = {str(i): i for i in range(1000)}
codeflash_output = _process_headers(headers)
def test_large_list_with_some_invalid_items():
"""Test a large list with a mix of valid and invalid items."""
headers = []
expected = {}
for i in range(1000):
if i % 10 == 0:
headers.append({"foo": "bar"}) # invalid
else:
headers.append({"key": str(i), "value": i})
expected[str(i)] = i
codeflash_output = _process_headers(headers)
def test_large_list_with_duplicate_keys():
"""Test that later duplicate keys overwrite earlier ones in large list."""
headers = []
for i in range(500):
headers.append({"key": "dup", "value": i})
expected = {"dup": 499}
codeflash_output = _process_headers(headers)
def test_large_list_with_varied_types():
"""Test large list with various key and value types."""
headers = []
expected = {}
for i in range(500):
headers.append({"key": i, "value": str(i)})
expected[i] = str(i)
headers.append({"key": str(i), "value": i})
expected[str(i)] = i
codeflash_output = _process_headers(headers)
# =========================
# 4. Regression/Mutation-resistance Tests
# =========================
def test_mutation_resistance_missing_continue():
"""If the continue is removed, invalid items will raise, so the function should not break."""
headers = [{"key": "A", "value": "B"}, 123]
# If 'continue' is removed, this would raise an error; must still return valid dict
codeflash_output = _process_headers(headers)
def test_mutation_resistance_wrong_field_names():
"""If the function uses wrong field names, it should not include those items."""
headers = [{"k": "A", "v": "B"}, {"key": "X", "value": "Y"}]
# Only the valid key/value dict should be included
codeflash_output = _process_headers(headers)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.To test or edit this optimization locally git merge codeflash/optimize-pr8388-2025-06-10T22.22.01
Click to see suggested changes
| try: | |
| for item in headers: | |
| if not _is_valid_key_value_item(item): | |
| continue | |
| key = item["key"] | |
| value = item["value"] | |
| processed_headers[key] = value | |
| except (KeyError, TypeError, ValueError): | |
| dict_type = dict # Local reference for speed | |
| try: | |
| # Inline and optimize _is_valid_key_value_item check | |
| for item in headers: | |
| if not isinstance(item, dict): | |
| continue | |
| # Try to only look up keys once per item | |
| try: | |
| key = item["key"] | |
| value = item["value"] | |
| except KeyError: | |
| continue # skip items lacking required keys | |
| processed_headers[key] = value | |
| except (TypeError, ValueError): |
|
@erichare could you please take the latest merge from main branch as to upgrade the codeflash version from uv.lock file which will fix the bug due to which we are recieving duplicate optimizations on the function _process_headers. |
There was a problem hiding this comment.
Actionable comments posted: 25
🔭 Outside diff range comments (2)
src/backend/base/langflow/components/data/mcp_component.py (1)
177-258: 🛠️ Refactor suggestionRefactor to reduce cyclomatic complexity
This method has 24 branches (exceeding the recommended 12), making it difficult to maintain and test. Consider breaking it into smaller, focused methods.
Extract the logic for each field_name into separate methods:
async def _handle_tool_field_update(self, build_config: dict, field_value: str) -> dict: # Handle tool field logic async def _handle_mcp_server_field_update(self, build_config: dict, field_value: str) -> dict: # Handle mcp_server field logic async def _handle_tool_mode_field_update(self, build_config: dict, field_value: str) -> dict: # Handle tool_mode field logic async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict: handlers = { "tool": self._handle_tool_field_update, "mcp_server": self._handle_mcp_server_field_update, "tool_mode": self._handle_tool_mode_field_update, } if field_name in handlers: return await handlers[field_name](build_config, field_value) return build_config🧰 Tools
🪛 Pylint (3.3.7)
[refactor] 179-257: Unnecessary "else" after "raise", remove the "else" and de-indent the code inside it
(R1720)
[refactor] 181-208: Unnecessary "else" after "raise", remove the "else" and de-indent the code inside it
(R1720)
[refactor] 177-177: Too many branches (24/12)
(R0912)
[refactor] 177-177: Too many statements (71/50)
(R0915)
src/backend/base/langflow/base/mcp/util.py (1)
508-595: 🛠️ Refactor suggestionConsider refactoring this complex function for better maintainability.
The function has high complexity with 19 local variables, 16 branches, and 54 statements. Consider breaking it down into smaller, focused functions:
- Extract server configuration validation
- Extract connection establishment logic
- Extract tool creation logic
Additionally, the broad exception handling (lines 555-557, 585-587, 590-593) might hide specific errors that could be useful for debugging.
Example refactoring approach:
async def _validate_server_config(server_name: str, server_config: dict) -> tuple[str, str, str]: """Validate and extract connection parameters from server config.""" mode = "Stdio" if "command" in server_config else "SSE" if "url" in server_config else "" command = server_config.get("command", "") url = server_config.get("url", "") await _validate_connection_params(mode, command, url) return mode, command, url async def _establish_connection(mode: str, server_config: dict, ...) -> tuple[list, Any]: """Establish connection based on mode and return tools and client.""" # Connection logic here async def _create_tool_objects(tools: list, client: Any, server_name: str) -> tuple[list[StructuredTool], dict]: """Create StructuredTool objects from tool definitions.""" # Tool creation logic hereWould you like me to provide a complete refactored implementation?
🧰 Tools
🪛 Pylint (3.3.7)
[refactor] 508-508: Too many local variables (19/15)
(R0914)
[refactor] 524-594: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it
(R1705)
[refactor] 508-508: Too many return statements (7/6)
(R0911)
[refactor] 508-508: Too many branches (16/12)
(R0912)
[refactor] 508-508: Too many statements (54/50)
(R0915)
♻️ Duplicate comments (2)
src/frontend/src/modals/addMcpServerModal/index.tsx (1)
44-62: Simplify state management patternThe conditional useState pattern is complex and the component has many state variables, which could lead to unnecessary re-renders.
Consider using
useReducerfor complex state management or extract the conditional logic:- const [open, setOpen] = - mySetOpen !== undefined && myOpen !== undefined - ? [myOpen, mySetOpen] - : useState(false); + const isControlled = mySetOpen !== undefined && myOpen !== undefined; + const [internalOpen, setInternalOpen] = useState(false); + const open = isControlled ? myOpen : internalOpen; + const setOpen = isControlled ? mySetOpen : setInternalOpen;src/backend/base/langflow/base/mcp/util.py (1)
227-256: Consider applying the performance optimization suggested by codeflash-ai.The current implementation has function call overhead that can be eliminated by inlining the validation logic. Additionally, the broad exception handling might hide specific errors that could be useful for debugging.
Apply the optimization suggested by codeflash-ai to improve performance:
-def _is_valid_key_value_item(item: Any) -> bool: - """Check if an item is a valid key-value dictionary.""" - return isinstance(item, dict) and "key" in item and "value" in item - - def _process_headers(headers: Any) -> dict: """Process the headers input into a valid dictionary. Args: headers: The headers to process, can be dict, str, or list Returns: Processed dictionary """ if headers is None: return {} if isinstance(headers, dict): return headers if isinstance(headers, list): - processed_headers = {} - try: - for item in headers: - if not _is_valid_key_value_item(item): - continue - key = item["key"] - value = item["value"] - processed_headers[key] = value - except (KeyError, TypeError, ValueError): - return {} # Return empty dictionary instead of None - return processed_headers + # Inline validation for better performance + return { + item["key"]: item["value"] + for item in headers + if isinstance(item, dict) and "key" in item and "value" in item + } return {}
🧹 Nitpick comments (31)
src/frontend/src/modals/IOModal/components/IOFieldView/components/key-pair-input.tsx (2)
69-77: Explicit button type to prevent form submission
Good catch addingtype="button"on the "add" button to avoid unintended form submissions.For accessibility, consider adding an
aria-label. For example:- <button - type="button" + <button + type="button" + aria-label="Add key-value pair"
83-91: Prevent unintended form submission on remove button
Thetype="button"attribute correctly avoids default form submits on the "remove" button.Likewise, add an
aria-labelfor screen readers, e.g.:- <button - type="button" + <button + type="button" + aria-label="Remove key-value pair"src/frontend/src/components/core/parameterRenderComponent/components/dropdownComponent/index.tsx (1)
16-17: Prop order nit – group the newplaceholderwith other visual props.Minor style/readability: consider locating
placeholderbesidecombobox&optionsso visual-only props stay together.No functional impact – feel free to ignore.
src/backend/base/langflow/io/__init__.py (1)
33-49: Duplicate entry in__all__still present
"DefaultPromptField"appears twice (lines 38-39). While unrelated to the current MCP change, keeping__all__de-duplicated avoids accidental shadowing and helps static-analysis tools.- "DefaultPromptField", - "DefaultPromptField", + "DefaultPromptField",src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/index.tsx (1)
476-478: Conditional render is fine; consider lazy importThe button is now hidden behind
ENABLE_DATASTAX_LANGFLOW, but the component is still imported unconditionally at line 24. If the store is rarely enabled, consider lazy-loading to avoid bundling code that most users never execute.-import { CustomStoreButton } from "@/customization/components/custom-store-button"; +const CustomStoreButton = + ENABLE_DATASTAX_LANGFLOW + ? (await import("@/customization/components/custom-store-button")) + .CustomStoreButton + : () => null;(simplified example – adapt to your build tooling)
src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons/index.tsx (2)
5-5: Avoid importing flags that remain hard-coded tofalse
ENABLE_LANGFLOW_STOREis currentlyfalseinfeature-flags.ts; importing it here means the compiler will tree-shake the whole block away in production builds.
If the intention is a staged rollout, consider:-import { ENABLE_LANGFLOW_STORE } from "@/customization/feature-flags"; +// import only when you actually intend to toggle it at build/run time +import { ENABLE_LANGFLOW_STORE } from "@/customization/feature-flags";Otherwise, remove the flag (and code) altogether during the cleanup pass to keep the bundle lean.
15-39: Left-over TODO comment – schedule removalThe wrapper comment “TODO: Remove this on cleanup” plus the feature-flag guard is a classic “will never be touched again” land-mine.
Either open a follow-up ticket or remove the block now if the Store won’t return; otherwise it risks drifting out of sync with the rest of the UI.src/frontend/src/components/core/dropdownComponent/index.tsx (1)
331-335: String fallback could ignore empty placeholder
placeholder || SELECT_AN_OPTIONmeans an empty string is treated as “no placeholder”.
If callers pass""intentionally you’ll still get “Select an option”.
Consider a nullish-coalescing check if that matters:-: placeholder || SELECT_AN_OPTION} +: (placeholder ?? SELECT_AN_OPTION)}src/frontend/tailwind.config.mjs (1)
491-503: Duplicate style object – minor DRY opportunityThe block for string colors and the
DEFAULTbranch are identical except for the background value.
Extracting a helper reduces duplication:const baseOverlay = (bg) => ({ position: "relative", overflow: "hidden", pointerEvents: "none", "&::after": { content: '""', position: "absolute", inset: "0 0 0 0", background: bg, }, }); ... acc[`.truncate-${className}`] = baseOverlay( `linear-gradient(to right, transparent 80%, ${colorValue})`, );Purely optional.
src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx (1)
81-88: Description field needs truncation guardWhen both
nameanddescriptionare long, the flex row can overflow.
Consider the sametruncateutilities on the description span.-<span className="font-normal text-muted-foreground"> +<span className="font-normal text-muted-foreground truncate">src/frontend/src/components/core/parameterRenderComponent/index.tsx (1)
283-292: Avoid redundant prop overrides when spreadingbaseInputProps
baseInputPropsalready containseditNode,disabled, andvalue.
Overriding them with identical values makes the JSX noisier and increases maintenance overhead.<McpComponent {...baseInputProps} id={`mcp_${id}`} - editNode={editNode} - disabled={disabled} - value={templateValue} />The explicit
idoverride is fine because it adds themcp_prefix, but the other duplicates are unnecessary.src/frontend/src/routes.tsx (1)
34-35: Eager import harms code-splitting
MCPServersPageis statically imported, whereas most heavy pages are lazy-loaded withReact.lazy.
This adds the whole settings page bundle to the initial chunk.-import MCPServersPage from "./pages/SettingsPage/pages/MCPServersPage"; +const MCPServersPage = lazy( + () => import("./pages/SettingsPage/pages/MCPServersPage"), +);Consider the lazy pattern for consistency and faster first paint.
src/frontend/src/customization/components/custom-store-sidebar.tsx (1)
3-36: Positional boolean parameters reduce clarityPassing two booleans (
true, ENABLE_LANGFLOW_STORE) at call sites is easy to mis-order.
An options object is clearer and extensible:-export const CustomStoreSidebar = ( - hasApiKey: boolean = false, - hasStore: boolean = false, -) => { +export const CustomStoreSidebar = ({ + hasApiKey = false, + hasStore = false, +}: { hasApiKey?: boolean; hasStore?: boolean } = {}) => {All callers then use:
CustomStoreSidebar({ hasApiKey: true, hasStore: ENABLE_LANGFLOW_STORE })src/frontend/src/pages/MainPage/pages/homePage/index.tsx (1)
7-10: Minor import grouping inconsistency
ENABLE_MCPandENABLE_DATASTAX_LANGFLOWcome from the same module but are split across two import blocks in some files and combined in others. Stick to one style to avoid noisy diffs.src/frontend/src/controllers/API/queries/mcp/use-get-mcp-servers.ts (1)
27-31: Static cache key limits cache-invalidationUsing a fixed key
["useGetMCPServers"]means callers can’t scope or invalidate by parameters (e.g., pagination, filters). At minimum, include an explicit version tag or a constant exported from a shared place to keep future additions manageable.src/frontend/src/style/applies.css (1)
192-196: Redundant definition – consider DRYing
.textarea-primarynow duplicates almost the whole.primary-inpututility. If the only difference is removingtruncate, factor out the common part to avoid future drift.src/frontend/src/types/mcp/index.ts (1)
11-16:toolsCountshould be optional or documented as always returnedEverywhere else in the codebase the tools count is computed on-demand. If an empty list could be returned by the API, making
toolsCountnon-optional will force callers to artificially coerce0. Either mark it as optional:-toolsCount: number; +toolsCount?: number;or guarantee the backend always sends the field. Please align both ends.
src/frontend/src/controllers/API/queries/mcp/use-add-mcp-server.ts (2)
23-37:headersfield silently discarded
MCPServerTypeincludesheaders, but the payload builder forgets it. Either remove the key from the type or forward it:if (body.headers && Object.keys(body.headers).length > 0) { payload.headers = body.headers; }
44-50: Error message copy-paste
"Failed to install MCP"is misleading for an “add server” endpoint. Replace with something like"Failed to add MCP Server".src/frontend/src/components/core/parameterRenderComponent/components/inputListComponent/index.tsx (1)
80-138: Overlay div copies value withopacity-0– potential layout shift for long stringsBecause the ghost span stays in flow, very long values will stretch the input container even though the real input scrolls. Consider replacing with
absolutepositioning or limiting max-width viaoverflow-hidden text-ellipsisto prevent unwanted reflows.src/frontend/src/components/ui/input.tsx (1)
5-10: Redefiningplaceholderduplicates DOM prop
React.InputHTMLAttributes<HTMLInputElement>already containsplaceholder?: string;. Redeclaring it is harmless but redundant and may confuse maintainers.src/frontend/src/pages/SettingsPage/pages/MCPServersPage/index.tsx (2)
36-40: Remove the emptyfinallyblock.The
finally {}adds no behaviour, but triggers an ESLint warning and hurts readability.- } finally { - } + }
24-26: AvoidanyforeditInitialData.You already have a typed shape (
MCPServerType | MCPServerInfoType).
Usinganyloses type-safety and auto-completion.-const [editInitialData, setEditInitialData] = useState<any>(null); +const [editInitialData, setEditInitialData] = + useState<MCPServerType | null>(null);src/frontend/src/controllers/API/queries/mcp/use-patch-mcp-server.ts (1)
23-37: Gracefully handle an empty PATCH payload.If the caller passes an object that only contains
name, the builtpayloadis{}– many back-ends treat an empty body as a 400.
Fail fast or warn the caller:if (Object.keys(payload).length === 0) { throw new Error("Nothing to patch – payload is empty"); }src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx (1)
173-200: Header search input never auto-focuses.Users have to click the field before typing, which breaks the quick-selection UX that power-users expect.
AddautoFocuswhen the dialog opens.-<Input +<Input + autoFocussrc/frontend/src/components/core/parameterRenderComponent/components/mcpComponent/index.tsx (1)
94-98: Accessibility: “Add MCP Server” button lacksaria-label.Screen-reader users cannot distinguish this button’s purpose. Add an
aria-labelor make the visible text descriptive.src/frontend/src/utils/mcpUtils.ts (1)
72-76: Remove redundant validation checkThe validation for
server.commandandserver.argsis redundant since these fields are already validated in the filtering logic above (lines 46-48, 54-55, and 62-63).- if (!server.command || !Array.isArray(server.args)) { - throw new Error( - "Each MCP server must have a 'command' and an 'args' array.", - ); - } return { name, server };src/frontend/src/modals/addMcpServerModal/index.tsx (1)
112-124: Extract parseEnvList to a utility functionThis function could be useful in other components and should be moved to a shared utility file.
Consider moving this to
src/frontend/src/utils/mcpUtils.tsalongside the other MCP utility functions.src/backend/base/langflow/api/v2/files.py (1)
189-196: Filter MCP_SERVERS_FILE at the SQL query levelFiltering files after fetching all records is inefficient. Filter at the SQL level for better performance.
- stmt = select(UserFile).where(UserFile.user_id == current_user.id) + stmt = select(UserFile).where( + UserFile.user_id == current_user.id, + UserFile.name != MCP_SERVERS_FILE + ) results = await session.exec(stmt) - - full_list = list(results) - - # Filter out the _mcp_servers file - return [file for file in full_list if file.name != MCP_SERVERS_FILE] + return list(results)src/backend/base/langflow/base/mcp/util.py (1)
334-363: Consider connection reuse for better performance.Creating a new connection and session for each tool call might be inefficient, especially when multiple tools are called in succession. Consider maintaining a persistent connection pool or reusing the session when possible.
Would you like me to suggest an implementation that maintains a connection pool or reuses sessions efficiently?
🧰 Tools
🪛 Pylint (3.3.7)
[error] 354-354: Using variable 'read' before assignment
(E0601)
[error] 354-354: Using variable 'write' before assignment
(E0601)
src/backend/base/langflow/api/v2/mcp.py (1)
111-126: Consider more specific exception handling for better debugging.The broad
except Exceptionclause might hide specific errors. Consider catching specific exceptions or at least logging the full exception type.- except Exception as e: # noqa: BLE001 - logger.exception(f"Error checking server {server_name}: {e}") + except (ConnectionError, TimeoutError, ValueError) as e: + logger.error(f"Error checking server {server_name}: {type(e).__name__}: {e}") + except Exception as e: + logger.exception(f"Unexpected error checking server {server_name}: {type(e).__name__}: {e}")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
src/frontend/package-lock.jsonis excluded by!**/package-lock.jsonuv.lockis excluded by!**/*.lock
📒 Files selected for processing (54)
.github/workflows/release.yml(0 hunks)pyproject.toml(2 hunks)src/backend/base/langflow/api/router.py(2 hunks)src/backend/base/langflow/api/v2/__init__.py(1 hunks)src/backend/base/langflow/api/v2/files.py(5 hunks)src/backend/base/langflow/api/v2/mcp.py(1 hunks)src/backend/base/langflow/base/mcp/util.py(7 hunks)src/backend/base/langflow/components/data/mcp_component.py(10 hunks)src/backend/base/langflow/inputs/__init__.py(2 hunks)src/backend/base/langflow/inputs/input_mixin.py(1 hunks)src/backend/base/langflow/inputs/inputs.py(2 hunks)src/backend/base/langflow/io/__init__.py(2 hunks)src/backend/base/langflow/utils/constants.py(1 hunks)src/backend/base/pyproject.toml(1 hunks)src/backend/tests/integration/components/mcp/test_mcp_component.py(1 hunks)src/backend/tests/unit/components/data/test_mcp_component.py(1 hunks)src/frontend/package.json(2 hunks)src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx(1 hunks)src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx(5 hunks)src/frontend/src/components/core/dropdownComponent/index.tsx(2 hunks)src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/index.tsx(1 hunks)src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx(1 hunks)src/frontend/src/components/core/parameterRenderComponent/components/dropdownComponent/index.tsx(2 hunks)src/frontend/src/components/core/parameterRenderComponent/components/inputListComponent/components/button-input-list.tsx(1 hunks)src/frontend/src/components/core/parameterRenderComponent/components/inputListComponent/index.tsx(3 hunks)src/frontend/src/components/core/parameterRenderComponent/components/mcpComponent/index.tsx(1 hunks)src/frontend/src/components/core/parameterRenderComponent/components/strRenderComponent/index.tsx(1 hunks)src/frontend/src/components/core/parameterRenderComponent/index.tsx(2 hunks)src/frontend/src/components/ui/input.tsx(1 hunks)src/frontend/src/constants/constants.ts(1 hunks)src/frontend/src/controllers/API/helpers/constants.ts(1 hunks)src/frontend/src/controllers/API/queries/mcp/use-add-mcp-server.ts(1 hunks)src/frontend/src/controllers/API/queries/mcp/use-delete-mcp-server.ts(1 hunks)src/frontend/src/controllers/API/queries/mcp/use-get-mcp-server.ts(1 hunks)src/frontend/src/controllers/API/queries/mcp/use-get-mcp-servers.ts(1 hunks)src/frontend/src/controllers/API/queries/mcp/use-patch-mcp-server.ts(1 hunks)src/frontend/src/customization/components/custom-store-sidebar.tsx(3 hunks)src/frontend/src/customization/feature-flags.ts(1 hunks)src/frontend/src/modals/IOModal/components/IOFieldView/components/key-pair-input.tsx(2 hunks)src/frontend/src/modals/addMcpServerModal/index.tsx(1 hunks)src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput/index.tsx(1 hunks)src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons/index.tsx(2 hunks)src/frontend/src/pages/MainPage/components/header/index.tsx(1 hunks)src/frontend/src/pages/MainPage/components/inputSearchComponent/index.tsx(1 hunks)src/frontend/src/pages/MainPage/pages/homePage/index.tsx(5 hunks)src/frontend/src/pages/SettingsPage/index.tsx(3 hunks)src/frontend/src/pages/SettingsPage/pages/MCPServersPage/index.tsx(1 hunks)src/frontend/src/routes.tsx(2 hunks)src/frontend/src/style/applies.css(2 hunks)src/frontend/src/types/mcp/index.ts(1 hunks)src/frontend/src/utils/mcpUtils.ts(1 hunks)src/frontend/tailwind.config.mjs(2 hunks)src/frontend/tests/extended/features/mcp-server-tab.spec.ts(1 hunks)src/frontend/tests/extended/features/mcp-server.spec.ts(2 hunks)
💤 Files with no reviewable changes (1)
- .github/workflows/release.yml
🧰 Additional context used
🧬 Code Graph Analysis (15)
src/backend/base/langflow/io/__init__.py (1)
src/backend/base/langflow/inputs/inputs.py (1)
McpInput(625-636)
src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx (1)
src/frontend/src/components/common/genericIconComponent/index.tsx (1)
ForwardedIconComponent(9-135)
src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons/index.tsx (1)
src/frontend/src/customization/feature-flags.ts (1)
ENABLE_LANGFLOW_STORE(3-3)
src/frontend/src/components/core/dropdownComponent/index.tsx (1)
src/frontend/src/constants/constants.ts (1)
SELECT_AN_OPTION(1013-1013)
src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/index.tsx (2)
src/frontend/src/customization/feature-flags.ts (1)
ENABLE_DATASTAX_LANGFLOW(10-10)src/frontend/src/customization/components/custom-store-button.tsx (1)
CustomStoreButton(4-21)
src/frontend/src/pages/SettingsPage/index.tsx (3)
src/frontend/src/components/common/genericIconComponent/index.tsx (1)
ForwardedIconComponent(9-135)src/frontend/src/customization/feature-flags.ts (2)
ENABLE_DATASTAX_LANGFLOW(10-10)ENABLE_LANGFLOW_STORE(3-3)src/frontend/src/customization/components/custom-store-sidebar.tsx (1)
CustomStoreSidebar(3-36)
src/backend/base/langflow/inputs/__init__.py (1)
src/backend/base/langflow/inputs/inputs.py (1)
McpInput(625-636)
src/backend/base/langflow/inputs/inputs.py (1)
src/backend/base/langflow/inputs/input_mixin.py (3)
BaseInputMixin(47-118)MetadataTraceMixin(129-130)FieldTypes(19-40)
src/frontend/src/controllers/API/queries/mcp/use-add-mcp-server.ts (4)
src/frontend/src/types/api/index.ts (1)
useMutationFunctionType(287-305)src/frontend/src/types/mcp/index.ts (1)
MCPServerType(18-25)src/frontend/src/controllers/API/services/request-processor.ts (1)
UseRequestProcessor(11-51)src/frontend/src/controllers/API/helpers/constants.ts (1)
getURL(36-44)
src/frontend/src/pages/MainPage/pages/homePage/index.tsx (1)
src/frontend/src/customization/feature-flags.ts (1)
ENABLE_MCP(16-16)
src/frontend/src/pages/SettingsPage/pages/MCPServersPage/index.tsx (4)
src/frontend/src/controllers/API/queries/mcp/use-get-mcp-servers.ts (1)
useGetMCPServers(9-32)src/frontend/src/controllers/API/queries/mcp/use-delete-mcp-server.ts (1)
useDeleteMCPServer(16-59)src/frontend/src/controllers/API/queries/mcp/use-get-mcp-server.ts (1)
useGetMCPServer(13-33)src/frontend/src/types/mcp/index.ts (1)
MCPServerInfoType(11-16)
src/frontend/src/routes.tsx (1)
src/frontend/src/pages/SettingsPage/pages/MCPServersPage/index.tsx (1)
MCPServersPage(19-172)
src/frontend/src/components/core/parameterRenderComponent/components/mcpComponent/index.tsx (5)
src/frontend/src/controllers/API/queries/mcp/use-get-mcp-servers.ts (1)
useGetMCPServers(9-32)src/frontend/src/components/ui/button.tsx (1)
Button(126-126)src/frontend/src/utils/utils.ts (1)
cn(36-38)src/frontend/src/components/common/genericIconComponent/index.tsx (1)
ForwardedIconComponent(9-135)src/frontend/src/modals/addMcpServerModal/index.tsx (1)
AddMcpServerModal(31-436)
src/frontend/tests/extended/features/mcp-server-tab.spec.ts (1)
src/frontend/tests/utils/zoom-out.ts (1)
zoomOut(3-7)
src/frontend/src/components/core/parameterRenderComponent/components/inputListComponent/index.tsx (1)
src/frontend/src/utils/utils.ts (1)
cn(36-38)
🪛 Pylint (3.3.7)
src/backend/tests/integration/components/mcp/test_mcp_component.py
[error] 9-9: No name 'components' in module 'langflow'
(E0611)
src/backend/base/langflow/api/v2/mcp.py
[refactor] 79-79: Too many arguments (6/5)
(R0913)
[refactor] 79-79: Too many positional arguments (6/5)
(R0917)
[refactor] 144-144: Too many arguments (8/5)
(R0913)
[refactor] 144-144: Too many positional arguments (6/5)
(R0917)
[refactor] 192-192: Too many arguments (6/5)
(R0913)
[refactor] 192-192: Too many positional arguments (6/5)
(R0917)
[refactor] 212-212: Too many arguments (6/5)
(R0913)
[refactor] 212-212: Too many positional arguments (6/5)
(R0917)
src/backend/base/langflow/base/mcp/util.py
[error] 317-317: Using variable 'read' before assignment
(E0601)
[error] 317-317: Using variable 'write' before assignment
(E0601)
[error] 354-354: Using variable 'read' before assignment
(E0601)
[error] 354-354: Using variable 'write' before assignment
(E0601)
[error] 447-447: Using variable 'read' before assignment
(E0601)
[error] 447-447: Using variable 'write' before assignment
(E0601)
[error] 490-490: Using variable 'read' before assignment
(E0601)
[error] 490-490: Using variable 'write' before assignment
(E0601)
[refactor] 508-508: Too many local variables (19/15)
(R0914)
[refactor] 524-594: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it
(R1705)
[refactor] 508-508: Too many return statements (7/6)
(R0911)
[refactor] 508-508: Too many branches (16/12)
(R0912)
[refactor] 508-508: Too many statements (54/50)
(R0915)
src/backend/base/langflow/components/data/mcp_component.py
[refactor] 181-208: Unnecessary "else" after "raise", remove the "else" and de-indent the code inside it
(R1720)
[refactor] 177-177: Too many branches (24/12)
(R0912)
[refactor] 177-177: Too many statements (71/50)
(R0915)
🔇 Additional comments (40)
src/frontend/src/pages/MainPage/components/header/index.tsx (1)
154-154: Styling update: apply consistent text sizeThe addition of
!text-mmdensures the search input matches the design system’s typography and aligns with container-level styling.src/frontend/package.json (1)
3-3: Version bump frontend to 1.4.3Aligns with backend and root version updates. No changes to dependencies detected.
src/backend/base/langflow/inputs/input_mixin.py (1)
40-40: Add MCP to FieldTypes enumEnables new MCP input type across the system. The existing validator will gracefully handle unknown values.
src/backend/base/langflow/api/router.py (2)
24-24: Import MCP v2 routerAliasing the v2
mcp_routerasmcp_router_v2follows established patterns and avoids naming conflicts.
57-57: Include MCP router in API v2Integrates the MCP endpoints under
/api/v2. Confirm thatmcp_router_v2defines its own prefix and tags.src/backend/base/pyproject.toml (1)
3-3: Bump base package version to 0.4.3Version alignment is consistent with the coordinated release strategy for backend and frontend.
src/frontend/src/customization/feature-flags.ts (1)
3-3: Disable Langflow Store feature flag
TheENABLE_LANGFLOW_STOREflag is now set tofalse, which will hide Store UI elements across the app. Verify that all conditional renders and related snapshots/tests have been updated accordingly.pyproject.toml (2)
3-3: Bump project version to 1.4.3
The project version has been incremented to1.4.3. Please confirm that the changelog, release notes, and CI/CD workflows reflect this new version.
20-20: Upgradelangflow-basedependency to 0.4.3
Thelangflow-basepin has been updated to0.4.3to align with backend MCP enhancements. Ensure compatibility, run full test suite, and verify there are no breaking changes.src/frontend/src/constants/constants.ts (1)
663-664: Add 'mcp' to supported flow types
The"mcp"entry has been added toLANGFLOW_SUPPORTED_TYPES, enabling frontend recognition of MCP inputs. Confirm that the parameter render component and type guards handle this new type correctly.src/backend/base/langflow/utils/constants.py (1)
73-73: Include 'mcp' in backend DIRECT_TYPES
The"mcp"type has been appended toDIRECT_TYPES, aligning backend type mapping with new MCP inputs. Ensure downstream serializers and validators accommodate this addition.src/frontend/src/controllers/API/helpers/constants.ts (1)
31-31: Add MCP_SERVERS API endpoint constant
IntroducedMCP_SERVERS: 'mcp/servers'to centralize the new MCP server management path. Verify that all CRUD hooks use this constant with the correctv2flag when callinggetURL.src/backend/base/langflow/api/v2/__init__.py (1)
2-7: Import/export looks good – nothing else to do.
mcp_routeris now publicly exposed and will be picked up byfrom langflow.api.v2 import *. No circular-import or ordering issues spotted.src/frontend/src/pages/MainPage/components/inputSearchComponent/index.tsx (1)
36-43: Layout change LGTM – verify icon alignment on narrow widths.Removing
absolutefrom the<Input>makes the element participate in normal flow while the search button remainsabsolute. Visually this should be fine as long as the input’s right padding (pr-12) keeps the text clear of the overlaying icon.
Please eyeball the component on mobile/smaller breakpoints to confirm no overlap/regression.src/frontend/src/components/core/parameterRenderComponent/components/strRenderComponent/index.tsx (1)
67-76:Details
❌ Incorrect review comment
Type definition may be missing
placeholderfor DropdownComponent.You forward
placeholder={placeholder}but theInputProps<string, DropDownComponentType>interface may not yet exposeplaceholderfor this branch. If that prop isn’t on the generic type, TypeScript will error downstream.// types.ts export interface DropDownComponentType { ... + placeholder?: string; }Double-check the shared
typesfile so that consumers ofDropdownComponentcompile cleanly.
🏁 Script executed:
#!/bin/bash # Search for the DropDownComponentType definition in the repo rg "interface.*DropDownComponentType" -n rg "interface.*DropdownComponentType" -nLength of output: 85
🏁 Script executed:
#!/bin/bash # Display import statements in the strRenderComponent file sed -n '1,50p' src/frontend/src/components/core/parameterRenderComponent/components/strRenderComponent/index.tsx # List all TS/TSX files under parameterRenderComponent to locate type definitions find src/frontend/src/components/core/parameterRenderComponent -type f \( -name "*.ts" -o -name "*.tsx" \) -print # If there’s a shared types file in that folder, print its contents TYPES_FILE=$(find src/frontend/src/components/core/parameterRenderComponent -type f -name "types.ts" | head -n1) if [ -n "$TYPES_FILE" ]; then echo "Found types file: $TYPES_FILE" sed -n '1,200p' "$TYPES_FILE" else echo "No types.ts file found in parameterRenderComponent" fi # Search within that directory for DropDownComponentType or similar typings rg "DropDownComponentType" -n src/frontend/src/components/core/parameterRenderComponent rg "DropdownComponentType" -n src/frontend/src/components/core/parameterRenderComponent rg "InputProps" -n src/frontend/src/components/core/parameterRenderComponentLength of output: 25257
No placeholder prop needed in DropDownComponentType
TheDropdownComponentsignature isInputProps<string, DropDownComponentType>, andInputPropsalready includesplaceholder(viaBaseInputPropsand its own{ placeholder?: string }). Passingplaceholder={placeholder}is therefore fully supported—no change toDropDownComponentTypeis required.Likely an incorrect or invalid review comment.
src/frontend/src/components/core/parameterRenderComponent/components/dropdownComponent/index.tsx (1)
42-43: Good catch passing placeholder through to base<Dropdown>.This ensures the UI shows a helpful hint when no value is selected.
src/backend/base/langflow/io/__init__.py (1)
15-15:McpInputcorrectly added to public interfaceImporting the new input here keeps the IO façade in sync with the wider MCP feature set—looks good.
src/frontend/src/components/core/parameterRenderComponent/components/inputListComponent/components/button-input-list.tsx (1)
30-31: Layout change: verify button overlap on all breakpointsReplacing the transform utilities with
-top-8 right-0could shift the clickable “add” hotspot in unexpected containers (e.g., RTL layouts or when parent has non-static positioning). Double-check on small screens and inside flex / grid parents.src/backend/base/langflow/inputs/__init__.py (1)
17-18: Import/__all__update aligns with new input type
McpInputis correctly surfaced here.src/backend/base/langflow/inputs/inputs.py (2)
625-637: MinimalMcpInputimplementation looks correctThe new class follows the pattern of other simple inputs and registers the proper
field_type. No further validators seem required for a plain string value.
676-677: Union & map updated – good catchAdding
McpInputtoInputTypesensures deserialisation works out-of-the-box, and the subsequentInputTypesMappick-up is automatic.src/frontend/src/components/core/dropdownComponent/index.tsx (1)
48-60:placeholderprop missing from type definition?
placeholderis destructured from props butDropDownComponent(imported from../../../types/components) didn’t previously expose it.
If that interface wasn’t updated, TypeScript will silently treat it asany, defeating the point of explicit typings.// types/components.d.ts (or equivalent) export interface DropDownComponent { ... + /** Optional text shown when no value selected and not in options */ + placeholder?: string; }Please verify the type file.
src/frontend/tailwind.config.mjs (1)
477-488: Good call disabling pointer events on gradient overlay
pointerEvents: "none"prevents the truncate overlay from hijacking clicks/selection – nice UX fix.
LGTM.src/frontend/src/pages/SettingsPage/index.tsx (1)
41-50:Details
✅ Verification successful
Verify that the
"Mcp"icon exists
ForwardedIconComponentwill log an error and render nothing if the icon can’t be resolved.
Run the check below; if the icon file is missing, add it or fall back to a generic icon.
🏁 Script executed:
#!/bin/bash # Search for an exported icon named 'Mcp' (case-insensitive) rg -i --line-number --context 2 $'export .*Mcp' | headLength of output: 507
🏁 Script executed:
#!/bin/bash # Locate the ForwardedIconComponent definition rg -i --line-number 'ForwardedIconComponent' -n src/frontend/srcLength of output: 40353
🏁 Script executed:
#!/bin/bash # List any icon file paths containing 'mcp' (case-insensitive) find src/frontend/src/icons -type f | grep -i mcpLength of output: 167
🏁 Script executed:
#!/bin/bash # Display the beginning of genericIconComponent to inspect icon resolution logic head -n 200 src/frontend/src/components/common/genericIconComponent/index.tsxLength of output: 4261
🏁 Script executed:
#!/bin/bash # Inspect getNodeIcon implementation rg -n "getNodeIcon" -n src/frontend/src/utils/styleUtils* # Inspect exported icons in the MCP icon directory head -n200 src/frontend/src/icons/MCP/index.tsxLength of output: 524
🏁 Script executed:
#!/bin/bash # Print the getNodeIcon implementation to verify icon resolution logic sed -n '460,600p' src/frontend/src/utils/styleUtils.tsLength of output: 1777
🏁 Script executed:
#!/bin/bash # Locate the definition of nodeIconToDisplayIconMap and check if 'Mcp' is mapped rg -n "nodeIconToDisplayIconMap" -C 5 src/frontend/src/utils/styleUtils.tsLength of output: 990
MCP icon verified
The
Mcpicon is present and correctly exported for lazy loading:
- Icon files exist under
src/frontend/src/icons/MCP/:
McpIcon.jsxindex.tsxexportingMcpIcongetNodeIcon( "Mcp" )will resolve to theMcpIconcomponent via the dynamic imports instyleUtils.ts.No fallback or generic‐icon changes are needed here.
src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx (1)
74-78: Removed fixed margin – visually OK?
mr-2on the icon was dropped and the parent gap bumped togap-3.
Check dense lists (e.g. dropdowns with many MCP servers) to ensure spacing isn’t excessive on narrow viewports.src/frontend/src/components/core/parameterRenderComponent/index.tsx (1)
19-19: Missing re-export check for new component
McpComponentis imported, but the closest barrel file (components/index.tsor equivalent) may need an updated export so downstream imports don’t have to reference the deep path.
Please verify that the new component is re-exported where expected.src/frontend/src/routes.tsx (1)
128-128: Access-guard parityThe new
/settings/mcp-serversroute is not wrapped inAuthSettingsGuard, unlikegeneral.
If MCP settings should be admin-only, wrap it; otherwise ignore this note.src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx (1)
127-138: Nice UX improvementDisabling the button when no actions exist and changing the label clarifies the state for users.
LGTM.src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchInput/index.tsx (2)
26-30: LGTM – aligns with newInputAPISwapping to the
icon&inputClassNameprops simplifies markup and matches the refactored UI primitives. No issues spotted.
36-37: Nice UX detailMoving the shortcut helper to the right keeps the left edge clear for the search icon, improving visual balance.
src/frontend/src/style/applies.css (1)
1250-1252: Visual regression checkReducing
.btn-add-input-listheight fromh-8toh-6affects click area and alignment. Confirm it still meets accessibility minimums (44 × 44 px touch target) in all contexts.src/frontend/src/types/mcp/index.ts (1)
18-25: Missingheadersin new-server payload helpersThe accompanying hooks (
useAddMCPServer,usePatchMCPServer) selectively serialise fields, butheadersis not included although it is part of the type. Verify whether headers are intentionally disallowed through the public API; if not, make sure the hooks forward it.src/frontend/tests/extended/features/mcp-server-tab.spec.ts (1)
244-246: Potentially empty config fed into the form.
fill(configJsonLinux || "")hides the fact thatconfigJsonLinuxcould beundefined, resulting in an empty submission that still passes the test. Assert the value before filling to avoid false positives.src/frontend/src/components/core/parameterRenderComponent/components/mcpComponent/index.tsx (1)
100-115:handleOnNewValueprop leaked to child that doesn’t accept it.
ListSelectionComponent’s prop list does not includehandleOnNewValue, yet it’s passed down, causing an implicitanyprop. Remove it unless intentionally used by a spread.src/frontend/tests/extended/features/mcp-server.spec.ts (1)
32-184: Well-structured test refactoringThe refactoring improves test reliability by:
- Removing manual retry loops in favor of explicit waits
- Using proper element selectors and assertions
- Following a clear flow for add, edit, and delete operations
src/backend/base/langflow/api/v2/files.py (1)
287-321: Well-structured file reading utilityThe function provides good abstraction for reading file content with proper error handling and UTF-8 decoding support. The docstring clearly explains the parameters and return values.
Consider consolidating the exception handling to reduce code duplication in the error messages.
src/backend/base/langflow/base/mcp/util.py (3)
24-79: Good migration to Pydantic v2 and improved error handling!The migration from Pydantic v1 to v2 APIs is implemented correctly, and the addition of error logging with contextual exception re-raising improves debugging capabilities.
259-281: Well-structured validation functions!The validation functions provide clear error messages and appropriate checks for Node.js installation and connection parameters.
465-500: Same connection reuse consideration as MCPStdioClient.🧰 Tools
🪛 Pylint (3.3.7)
[error] 490-490: Using variable 'read' before assignment
(E0601)
[error] 490-490: Using variable 'write' before assignment
(E0601)
src/backend/base/langflow/api/v2/mcp.py (1)
15-35: Clean implementation for server config upload!The conversion from dict to UploadFile through JSON serialization is well-implemented.
| # TODO: Add more tests for MCPToolsComponent | ||
| @pytest.mark.asyncio | ||
| async def test_mcp_component(): | ||
| from langflow.components.data.mcp_component import MCPToolsComponent | ||
|
|
||
| inputs = {} | ||
| await run_single_component( | ||
| MCPToolsComponent, | ||
| inputs=inputs, # test default inputs | ||
| ) | ||
|
|
||
| # Expect an error from this call | ||
| with pytest.raises(ValueError, match="None"): | ||
| await run_single_component( | ||
| MCPToolsComponent, | ||
| inputs=inputs, | ||
| ) |
There was a problem hiding this comment.
Assertion on error message is fragile – match the exception type only.
Coupling the test to the exact string "None" risks breakage if the internal message wording changes. It’s enough to assert that a ValueError is raised.
-# Expect an error from this call
-with pytest.raises(ValueError, match="None"):
+# Expect a ValueError from this call
+with pytest.raises(ValueError):If you really need to assert on the message, consider checking for a semantic fragment such as "No MCP server" instead of "None".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # TODO: Add more tests for MCPToolsComponent | |
| @pytest.mark.asyncio | |
| async def test_mcp_component(): | |
| from langflow.components.data.mcp_component import MCPToolsComponent | |
| inputs = {} | |
| await run_single_component( | |
| MCPToolsComponent, | |
| inputs=inputs, # test default inputs | |
| ) | |
| # Expect an error from this call | |
| with pytest.raises(ValueError, match="None"): | |
| await run_single_component( | |
| MCPToolsComponent, | |
| inputs=inputs, | |
| ) | |
| # TODO: Add more tests for MCPToolsComponent | |
| @pytest.mark.asyncio | |
| async def test_mcp_component(): | |
| from langflow.components.data.mcp_component import MCPToolsComponent | |
| inputs = {} | |
| # Expect a ValueError from this call | |
| with pytest.raises(ValueError): | |
| await run_single_component( | |
| MCPToolsComponent, | |
| inputs=inputs, | |
| ) |
🧰 Tools
🪛 Pylint (3.3.7)
[error] 9-9: No name 'components' in module 'langflow'
(E0611)
🤖 Prompt for AI Agents
In src/backend/tests/integration/components/mcp/test_mcp_component.py around
lines 6 to 18, the test asserts the error message string "None" when expecting a
ValueError, which is fragile. Modify the test to only assert that a ValueError
is raised without matching the exact message, or if message checking is
necessary, match a more meaningful fragment like "No MCP server" to reduce
brittleness.
| // TODO: Remove this on cleanup | ||
| if (!ENABLE_DATASTAX_LANGFLOW) { | ||
| const langflowItems = CustomStoreSidebar(); | ||
|
|
||
| const langflowItems = CustomStoreSidebar(true, ENABLE_LANGFLOW_STORE); | ||
| sidebarNavItems.splice(2, 0, ...langflowItems); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Index-based splice fragile to earlier pushes
sidebarNavItems.splice(2, 0, …) assumes at least two items already exist.
If showGeneralSettings is false, the array length is one when this splice runs, so store items land at the end, changing UX order.
Safer:
- sidebarNavItems.splice(2, 0, ...langflowItems);
+ const insertAt = Math.min(2, sidebarNavItems.length);
+ sidebarNavItems.splice(insertAt, 0, ...langflowItems);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // TODO: Remove this on cleanup | |
| if (!ENABLE_DATASTAX_LANGFLOW) { | |
| const langflowItems = CustomStoreSidebar(); | |
| const langflowItems = CustomStoreSidebar(true, ENABLE_LANGFLOW_STORE); | |
| sidebarNavItems.splice(2, 0, ...langflowItems); | |
| } | |
| // TODO: Remove this on cleanup | |
| if (!ENABLE_DATASTAX_LANGFLOW) { | |
| const langflowItems = CustomStoreSidebar(true, ENABLE_LANGFLOW_STORE); | |
| const insertAt = Math.min(2, sidebarNavItems.length); | |
| sidebarNavItems.splice(insertAt, 0, ...langflowItems); | |
| } |
🤖 Prompt for AI Agents
In src/frontend/src/pages/SettingsPage/index.tsx around lines 84 to 88, the
splice operation uses a fixed index 2 which assumes sidebarNavItems has at least
two items, but if showGeneralSettings is false, the array length is only one,
causing the inserted items to appear at the end and disrupt the intended order.
To fix this, dynamically determine the correct insertion index based on the
current length or presence of items in sidebarNavItems before splicing, ensuring
the new items are inserted at the intended position regardless of array length.
| # TODO: This test suite is incomplete and is in need of an update to handle the latest MCP component changes. | ||
| pytestmark = pytest.mark.skip(reason="Skipping entire file") | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Whole test suite skipped – coverage regression
Marking the file with a blanket skip hides regressions and silently reduces confidence.
Prefer converting failing cases to xfail or updating the tests to reflect the new API.
-pytestmark = pytest.mark.skip(reason="Skipping entire file")
+pytestmark = pytest.mark.xfail(reason="Outdated – needs refactor", strict=False)xfail keeps the tests visible while preventing pipeline failure.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # TODO: This test suite is incomplete and is in need of an update to handle the latest MCP component changes. | |
| pytestmark = pytest.mark.skip(reason="Skipping entire file") | |
| # TODO: This test suite is incomplete and is in need of an update to handle the latest MCP component changes. | |
| pytestmark = pytest.mark.xfail(reason="Outdated – needs refactor", strict=False) |
🤖 Prompt for AI Agents
In src/backend/tests/unit/components/data/test_mcp_component.py around lines 9
to 11, the entire test suite is currently skipped using pytest.mark.skip, which
hides test failures and reduces coverage visibility. Replace the skip marker
with pytest.mark.xfail on individual failing tests or update the tests to align
with the latest MCP component changes. This will keep the tests visible in the
pipeline while preventing failures from blocking progress.
| const isEmptyFolder = | ||
| flows?.find((flow) => flow.folder_id === (folderId ?? myCollectionId)) === | ||
| undefined; | ||
| flows?.find( | ||
| (flow) => | ||
| flow.folder_id === (folderId ?? myCollectionId) && | ||
| (ENABLE_MCP ? flow.is_component === false : true), | ||
| ) === undefined; | ||
|
|
There was a problem hiding this comment.
isEmptyFolder no longer respects current flowType – breaks component view
Switching the condition to
ENABLE_MCP ? flow.is_component === false : trueignores the selected flowType.
If flowType === "components" and the flag is enabled, the check always filters out components, making every components-only folder appear empty and disabling the whole UI path that follows (handleFileDrop, empty-state banner, etc.).
- (ENABLE_MCP ? flow.is_component === false : true)
+ flow.is_component === (flowType === "components")Retain the flag gate elsewhere (e.g., by allowing the MCP type to short-circuit this computation), but don’t change semantics for the existing two modes.
🤖 Prompt for AI Agents
In src/frontend/src/pages/MainPage/pages/homePage/index.tsx around lines 82 to
88, the isEmptyFolder condition incorrectly ignores the current flowType when
ENABLE_MCP is true by always filtering out components. To fix this, restore the
original flowType-based filtering logic and move the ENABLE_MCP flag handling
outside this condition, allowing MCP type to short-circuit the computation
without altering the existing behavior for other flowTypes.
| const responseFn = async () => { | ||
| try { | ||
| const { data } = await api.get<getMCPServersResponse>( | ||
| `${getURL("MCP_SERVERS", undefined, true)}`, | ||
| ); | ||
| return data; | ||
| } catch (error) { | ||
| console.error(error); | ||
| return []; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Error swallowing hides useful React-Query states
Catching the request and returning [] prevents React-Query from ever reaching an error state; upstream components can’t show “failed to load servers”.
Prefer re-throwing (and handling with onError) or mapping to a typed ServerError.
- } catch (error) {
- console.error(error);
- return [];
- }
+ } catch (error) {
+ // Let React-Query propagate the failure
+ throw error;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const responseFn = async () => { | |
| try { | |
| const { data } = await api.get<getMCPServersResponse>( | |
| `${getURL("MCP_SERVERS", undefined, true)}`, | |
| ); | |
| return data; | |
| } catch (error) { | |
| console.error(error); | |
| return []; | |
| } | |
| }; | |
| const responseFn = async () => { | |
| try { | |
| const { data } = await api.get<getMCPServersResponse>( | |
| `${getURL("MCP_SERVERS", undefined, true)}`, | |
| ); | |
| return data; | |
| } catch (error) { | |
| // Let React-Query propagate the failure | |
| throw error; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In src/frontend/src/controllers/API/queries/mcp/use-get-mcp-servers.ts around
lines 15 to 25, the current catch block swallows errors by logging them and
returning an empty array, preventing React-Query from detecting error states.
Modify the catch block to re-throw the caught error instead of returning [], so
React-Query can handle the error properly and upstream components can respond to
loading failures. Optionally, you can map the error to a typed ServerError
before re-throwing for better error handling.
| elif hasattr(file_input, "read"): | ||
| while True: | ||
| chunk = await file_input.read(chunk_size) if callable(file_input.read) else file_input.read(chunk_size) | ||
| if not chunk: | ||
| break | ||
| yield chunk |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Clarify async/sync read method handling
The callable check logic is unclear and could fail for certain stream types. Consider explicitly checking for async methods.
elif hasattr(file_input, "read"):
while True:
- chunk = await file_input.read(chunk_size) if callable(file_input.read) else file_input.read(chunk_size)
+ # Check if it's an async read method
+ if hasattr(file_input.read, "__await__") or hasattr(file_input.read, "__call__"):
+ chunk = await file_input.read(chunk_size)
+ else:
+ chunk = file_input.read(chunk_size)
if not chunk:
break
yield chunk📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| elif hasattr(file_input, "read"): | |
| while True: | |
| chunk = await file_input.read(chunk_size) if callable(file_input.read) else file_input.read(chunk_size) | |
| if not chunk: | |
| break | |
| yield chunk | |
| elif hasattr(file_input, "read"): | |
| while True: | |
| # Check if it's an async read method | |
| if hasattr(file_input.read, "__await__") or hasattr(file_input.read, "__call__"): | |
| chunk = await file_input.read(chunk_size) | |
| else: | |
| chunk = file_input.read(chunk_size) | |
| if not chunk: | |
| break | |
| yield chunk |
🤖 Prompt for AI Agents
In src/backend/base/langflow/api/v2/files.py around lines 35 to 40, the current
code uses a callable check on file_input.read which is unclear and may fail for
some stream types. Update the code to explicitly check if file_input.read is an
async method (e.g., using asyncio.iscoroutinefunction) and await it accordingly;
otherwise, call it synchronously. This will clarify the handling of async versus
sync read methods and ensure proper reading of chunks.
|
|
||
| class MCPToolsComponent(Component): | ||
| schema_inputs: list[InputTypes] = [] | ||
| schema_inputs: list = [] |
There was a problem hiding this comment.
Avoid mutable default value for class attribute
Using a mutable default value (empty list) for a class attribute can lead to shared state between instances.
- schema_inputs: list = []
+ schema_inputs: list = NoneThen initialize it in __init__ if needed:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.schema_inputs is None:
self.schema_inputs = []🤖 Prompt for AI Agents
In src/backend/base/langflow/components/data/mcp_component.py at line 63, the
class attribute schema_inputs is assigned a mutable default value (an empty
list), which can cause shared state across instances. To fix this, remove the
default empty list assignment from the class attribute and instead initialize
schema_inputs to an empty list inside the __init__ method, checking if it is
None before assignment to ensure each instance has its own separate list.
| if self.tool != "": | ||
| exec_tool = self._tool_cache[self.tool] | ||
| tool_args = self.get_inputs_for_all_tools(self.tools)[self.tool] |
There was a problem hiding this comment.
Add validation before accessing tool cache
Accessing self._tool_cache[self.tool] without checking if the key exists could raise a KeyError.
if self.tool != "":
+ if self.tool not in self._tool_cache:
+ return DataFrame(data=[{"error": f"Tool '{self.tool}' not found in cache"}])
exec_tool = self._tool_cache[self.tool]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if self.tool != "": | |
| exec_tool = self._tool_cache[self.tool] | |
| tool_args = self.get_inputs_for_all_tools(self.tools)[self.tool] | |
| if self.tool != "": | |
| if self.tool not in self._tool_cache: | |
| return DataFrame(data=[{"error": f"Tool '{self.tool}' not found in cache"}]) | |
| exec_tool = self._tool_cache[self.tool] | |
| tool_args = self.get_inputs_for_all_tools(self.tools)[self.tool] |
🤖 Prompt for AI Agents
In src/backend/base/langflow/components/data/mcp_component.py around lines 365
to 367, add a validation check to ensure that self.tool exists as a key in
self._tool_cache before accessing it. If the key is not present, handle the case
appropriately to avoid a KeyError, such as by returning early, raising a
controlled exception, or providing a default value.
| try: | ||
| async for db in get_session(): | ||
| user_id, _ = await create_user_longterm_token(db) | ||
| current_user = await get_user_by_id(db, user_id) | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Optimize user token creation
Creating a new long-term token on every tool list update is inefficient and could impact performance. Consider caching the user or passing it as a parameter.
🏁 Script executed:
#!/bin/bash
# Check how frequently update_tool_list is called
rg -A 5 "update_tool_list" --type pyLength of output: 4002
🏁 Script executed:
#!/bin/bash
# 1. Show update_tool_list context
sed -n '130,180p' src/backend/base/langflow/components/data/mcp_component.py
# 2. Locate create_user_longterm_token definition
rg -C5 "def create_user_longterm_token" --type py .
# 3. Locate get_user_by_id definition
rg -C5 "def get_user_by_id" --type py .Length of output: 4057
🏁 Script executed:
#!/bin/bash
# Display the full create_user_longterm_token function for inspection
sed -n '1,200p' src/backend/base/langflow/services/auth/utils.pyLength of output: 7901
🏁 Script executed:
#!/bin/bash
# Find line number of create_user_longterm_token
rg -n "async def create_user_longterm_token" src/backend/base/langflow/services/auth/utils.py
# Display the function body around that line
sed -n '1,200p' src/backend/base/langflow/services/auth/utils.py | sed -n '1,200p'Length of output: 8094
Avoid regenerating the user token on each update
The update_tool_list method (src/backend/base/langflow/components/data/mcp_component.py:~142–150) currently does:
async for db in get_session():
user_id, _ = await create_user_longterm_token(db)
current_user = await get_user_by_id(db, user_id)
…Calling create_user_longterm_token here triggers a new JWT creation and a DB write (update_user_last_login_at) every time update_tool_list runs. Since this method is invoked repeatedly (UI updates, form changes, etc.), it can noticeably degrade performance.
Suggestions:
- Generate and cache the token (and/or the resolved
current_user) once—e.g., during component initialization or the first call—and reuse it thereafter. - Alternatively, accept the already-authenticated user or token as a parameter to
update_tool_listinstead of re-creating it.
• File: src/backend/base/langflow/components/data/mcp_component.py
• Lines: 142–150
🤖 Prompt for AI Agents
In src/backend/base/langflow/components/data/mcp_component.py around lines 145
to 149, the code calls create_user_longterm_token and get_user_by_id inside
update_tool_list on every invocation, causing unnecessary token regeneration and
DB writes. To fix this, refactor update_tool_list to accept the current_user or
token as a parameter or cache the user/token during component initialization or
the first call, then reuse it on subsequent calls to avoid repeated token
creation and improve performance.
| # Remove the existing file | ||
| server_config_file = await get_file_by_name(MCP_SERVERS_FILE, current_user, session) | ||
|
|
||
| if server_config_file: | ||
| await delete_file(server_config_file.id, current_user, session, storage_service) | ||
|
|
||
| # Upload the updated server configuration | ||
| await upload_server_config( | ||
| server_list, current_user, session, storage_service=storage_service, settings_service=settings_service | ||
| ) |
There was a problem hiding this comment.
Potential race condition in atomic file updates.
The current approach of deleting and re-uploading the file could lead to race conditions if multiple requests attempt to update the server configuration simultaneously. Consider implementing file locking or using a more atomic update mechanism.
Would you like me to suggest an implementation using file locking or a database-based approach for more reliable concurrent updates?
🤖 Prompt for AI Agents
In src/backend/base/langflow/api/v2/mcp.py around lines 170 to 179, the current
method deletes and then re-uploads the server configuration file, which can
cause race conditions with concurrent updates. To fix this, implement a file
locking mechanism to ensure only one update occurs at a time, or switch to an
atomic update approach such as writing to a temporary file and then renaming it.
Alternatively, consider using a database transaction or versioning system to
handle concurrent updates safely.
This pull request introduces a new "MCP" (Managed Connection Protocol) feature to the backend API and updates existing components to support it. Key changes include adding a new
mcp_routerwith endpoints for managing MCP server configurations, enhancing file handling utilities, and refactoring theMCPToolsComponentto integrate with the MCP server functionality.Backend API Enhancements:
mcp_routerto the API, providing endpoints for managing MCP server configurations, including listing, adding, updating, and deleting servers (src/backend/base/langflow/api/v2/mcp.py).mcp_routerunder the/api/v2prefix (src/backend/base/langflow/api/router.py). [1] [2]File Handling Improvements:
byte_stream_generatorfunction to support streams and async iterables, improving flexibility in file handling (src/backend/base/langflow/api/v2/files.py).read_file_contentutility for reading file content as bytes or UTF-8 strings, with error handling for invalid encodings (src/backend/base/langflow/api/v2/files.py)._mcp_serversfile (src/backend/base/langflow/api/v2/files.py).MCPToolsComponent Refactor:
McpInputfor selecting MCP servers, simplifying the component's configuration (src/backend/base/langflow/components/data/mcp_component.py).command,env, andsse_urlto streamline the component's schema (src/backend/base/langflow/components/data/mcp_component.py).src/backend/base/langflow/components/data/mcp_component.py).Other Changes:
AsyncIterableimport to support new file handling utilities (src/backend/base/langflow/api/v2/files.py).__init__.pyto export the newmcp_routerfor API integration (src/backend/base/langflow/api/v2/__init__.py).Summary by CodeRabbit
New Features
Enhancements
Bug Fixes
Chores
Documentation
Tests