Skip to content

feat: add servers persistence to MCP connection component, add MCP connections settings page#8388

Merged
lucaseduoli merged 92 commits into
mainfrom
feat/mcp_persistence
Jun 11, 2025
Merged

feat: add servers persistence to MCP connection component, add MCP connections settings page#8388
lucaseduoli merged 92 commits into
mainfrom
feat/mcp_persistence

Conversation

@lucaseduoli
Copy link
Copy Markdown
Collaborator

@lucaseduoli lucaseduoli commented Jun 5, 2025

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_router with endpoints for managing MCP server configurations, enhancing file handling utilities, and refactoring the MCPToolsComponent to integrate with the MCP server functionality.

Backend API Enhancements:

  • Added a new mcp_router to the API, providing endpoints for managing MCP server configurations, including listing, adding, updating, and deleting servers (src/backend/base/langflow/api/v2/mcp.py).
  • Updated the main API router to include the mcp_router under the /api/v2 prefix (src/backend/base/langflow/api/router.py). [1] [2]

File Handling Improvements:

  • Enhanced the byte_stream_generator function to support streams and async iterables, improving flexibility in file handling (src/backend/base/langflow/api/v2/files.py).
  • Introduced a new read_file_content utility for reading file content as bytes or UTF-8 strings, with error handling for invalid encodings (src/backend/base/langflow/api/v2/files.py).
  • Modified file listing to exclude the reserved _mcp_servers file (src/backend/base/langflow/api/v2/files.py).

MCPToolsComponent Refactor:

  • Replaced the "Mode" input with a new McpInput for selecting MCP servers, simplifying the component's configuration (src/backend/base/langflow/components/data/mcp_component.py).
  • Removed unused inputs like command, env, and sse_url to streamline the component's schema (src/backend/base/langflow/components/data/mcp_component.py).
  • Updated internal logic to fetch tools directly from the selected MCP server, removing mode-specific configurations (src/backend/base/langflow/components/data/mcp_component.py).

Other Changes:

  • Added AsyncIterable import to support new file handling utilities (src/backend/base/langflow/api/v2/files.py).
  • Updated __init__.py to export the new mcp_router for API integration (src/backend/base/langflow/api/v2/__init__.py).

Summary by CodeRabbit

  • New Features

    • Introduced comprehensive MCP (Multi-Channel Processing) server management, including backend API endpoints and frontend interfaces for adding, editing, and deleting MCP servers.
    • Added a dedicated MCP Connections management page in settings, with UI for listing, searching, and managing MCP servers and their actions.
    • Implemented a modal dialog for adding or updating MCP servers, supporting multiple input modes (JSON, STDIO, SSE).
    • Integrated MCP server selection as a new input type in components, enabling dynamic tool loading from MCP servers.
  • Enhancements

    • Improved dropdown, input, and list UI components with new placeholder support, refined layout, and better feedback when no actions are available.
    • Updated styles and layout for various UI elements, including overlays, sidebars, and text areas.
    • Enhanced error handling and feedback in both backend and frontend for file downloads, server management, and input validation.
  • Bug Fixes

    • Refined test logic for MCP server UI to ensure reliable interactions and assertions without manual retry loops.
  • Chores

    • Updated project versions and removed a maintainer from configuration files.
    • Disabled incomplete or outdated unit tests for MCP components.
  • Documentation

    • Added and updated TypeScript types for MCP server data structures.
  • Tests

    • Improved and refactored end-to-end tests for MCP server management features.

lucaseduoli and others added 30 commits June 3, 2025 12:20
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>
@github-actions github-actions Bot added the enhancement New feature or request label Jun 10, 2025
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 10, 2025
codeflash-ai Bot added a commit that referenced this pull request Jun 10, 2025
…_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.
Comment on lines +245 to +255
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡️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_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.

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

Suggested change
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
}

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 10, 2025
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 10, 2025
Comment on lines +246 to +253
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):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡️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_item in _process_headers to 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 catch KeyError` 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 item and "value" in item" lookups.
  • Catches only the needed exceptions at the narrowest possible scope.
  • Kept reference to dict local 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
Suggested change
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):

@Saga4
Copy link
Copy Markdown

Saga4 commented Jun 10, 2025

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

🔭 Outside diff range comments (2)
src/backend/base/langflow/components/data/mcp_component.py (1)

177-258: 🛠️ Refactor suggestion

Refactor 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 suggestion

Consider 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:

  1. Extract server configuration validation
  2. Extract connection establishment logic
  3. 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 here

Would 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 pattern

The conditional useState pattern is complex and the component has many state variables, which could lead to unnecessary re-renders.

Consider using useReducer for 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 adding type="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
The type="button" attribute correctly avoids default form submits on the "remove" button.

Likewise, add an aria-label for 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 new placeholder with other visual props.

Minor style/readability: consider locating placeholder beside combobox & options so 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 import

The 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 to false

ENABLE_LANGFLOW_STORE is currently false in feature-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 removal

The 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_OPTION means 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 opportunity

The block for string colors and the DEFAULT branch 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 guard

When both name and description are long, the flex row can overflow.
Consider the same truncate utilities 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 spreading baseInputProps

baseInputProps already contains editNode, disabled, and value.
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 id override is fine because it adds the mcp_ prefix, but the other duplicates are unnecessary.

src/frontend/src/routes.tsx (1)

34-35: Eager import harms code-splitting

MCPServersPage is statically imported, whereas most heavy pages are lazy-loaded with React.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 clarity

Passing 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_MCP and ENABLE_DATASTAX_LANGFLOW come 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-invalidation

Using 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-primary now duplicates almost the whole .primary-input utility. If the only difference is removing truncate, factor out the common part to avoid future drift.

src/frontend/src/types/mcp/index.ts (1)

11-16: toolsCount should be optional or documented as always returned

Everywhere else in the codebase the tools count is computed on-demand. If an empty list could be returned by the API, making toolsCount non-optional will force callers to artificially coerce 0. 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: headers field silently discarded

MCPServerType includes headers, 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 with opacity-0 – potential layout shift for long strings

Because the ghost span stays in flow, very long values will stretch the input container even though the real input scrolls. Consider replacing with absolute positioning or limiting max-width via overflow-hidden text-ellipsis to prevent unwanted reflows.

src/frontend/src/components/ui/input.tsx (1)

5-10: Redefining placeholder duplicates DOM prop

React.InputHTMLAttributes<HTMLInputElement> already contains placeholder?: 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 empty finally block.

The finally {} adds no behaviour, but triggers an ESLint warning and hurts readability.

-    } finally {
-    }
+    }

24-26: Avoid any for editInitialData.

You already have a typed shape (MCPServerType | MCPServerInfoType).
Using any loses 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 built payload is {} – 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.
Add autoFocus when the dialog opens.

-<Input
+<Input
+  autoFocus
src/frontend/src/components/core/parameterRenderComponent/components/mcpComponent/index.tsx (1)

94-98: Accessibility: “Add MCP Server” button lacks aria-label.

Screen-reader users cannot distinguish this button’s purpose. Add an aria-label or make the visible text descriptive.

src/frontend/src/utils/mcpUtils.ts (1)

72-76: Remove redundant validation check

The validation for server.command and server.args is 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 function

This 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.ts alongside the other MCP utility functions.

src/backend/base/langflow/api/v2/files.py (1)

189-196: Filter MCP_SERVERS_FILE at the SQL query level

Filtering 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 Exception clause 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

📥 Commits

Reviewing files that changed from the base of the PR and between 97dd5b3 and 7a45e22.

⛔ Files ignored due to path filters (2)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
  • uv.lock is 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 size

The addition of !text-mmd ensures 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.3

Aligns 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 enum

Enables 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 router

Aliasing the v2 mcp_router as mcp_router_v2 follows established patterns and avoids naming conflicts.


57-57: Include MCP router in API v2

Integrates the MCP endpoints under /api/v2. Confirm that mcp_router_v2 defines its own prefix and tags.

src/backend/base/pyproject.toml (1)

3-3: Bump base package version to 0.4.3

Version 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
The ENABLE_LANGFLOW_STORE flag is now set to false, 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 to 1.4.3. Please confirm that the changelog, release notes, and CI/CD workflows reflect this new version.


20-20: Upgrade langflow-base dependency to 0.4.3
The langflow-base pin has been updated to 0.4.3 to 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 to LANGFLOW_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 to DIRECT_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
Introduced MCP_SERVERS: 'mcp/servers' to centralize the new MCP server management path. Verify that all CRUD hooks use this constant with the correct v2 flag when calling getURL.

src/backend/base/langflow/api/v2/__init__.py (1)

2-7: Import/export looks good – nothing else to do.

mcp_router is now publicly exposed and will be picked up by from 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 absolute from the <Input> makes the element participate in normal flow while the search button remains absolute. 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 placeholder for DropdownComponent.

You forward placeholder={placeholder} but the InputProps<string, DropDownComponentType> interface may not yet expose placeholder for 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 types file so that consumers of DropdownComponent compile cleanly.


🏁 Script executed:

#!/bin/bash
# Search for the DropDownComponentType definition in the repo
rg "interface.*DropDownComponentType" -n
rg "interface.*DropdownComponentType" -n

Length 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/parameterRenderComponent

Length of output: 25257


No placeholder prop needed in DropDownComponentType
The DropdownComponent signature is InputProps<string, DropDownComponentType>, and InputProps already includes placeholder (via BaseInputProps and its own { placeholder?: string }). Passing placeholder={placeholder} is therefore fully supported—no change to DropDownComponentType is 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: McpInput correctly added to public interface

Importing 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 breakpoints

Replacing the transform utilities with -top-8 right-0 could 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

McpInput is correctly surfaced here.

src/backend/base/langflow/inputs/inputs.py (2)

625-637: Minimal McpInput implementation looks correct

The 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 catch

Adding McpInput to InputTypes ensures deserialisation works out-of-the-box, and the subsequent InputTypesMap pick-up is automatic.

src/frontend/src/components/core/dropdownComponent/index.tsx (1)

48-60: placeholder prop missing from type definition?

placeholder is destructured from props but DropDownComponent (imported from ../../../types/components) didn’t previously expose it.
If that interface wasn’t updated, TypeScript will silently treat it as any, 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

ForwardedIconComponent will 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' | head

Length of output: 507


🏁 Script executed:

#!/bin/bash
# Locate the ForwardedIconComponent definition
rg -i --line-number 'ForwardedIconComponent' -n src/frontend/src

Length 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 mcp

Length 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.tsx

Length 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.tsx

Length 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.ts

Length 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.ts

Length of output: 990


MCP icon verified

The Mcp icon is present and correctly exported for lazy loading:

  • Icon files exist under src/frontend/src/icons/MCP/:
    • McpIcon.jsx
    • index.tsx exporting McpIcon
  • getNodeIcon( "Mcp" ) will resolve to the McpIcon component via the dynamic imports in styleUtils.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-2 on the icon was dropped and the parent gap bumped to gap-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

McpComponent is imported, but the closest barrel file (components/index.ts or 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 parity

The new /settings/mcp-servers route is not wrapped in AuthSettingsGuard, unlike general.
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 improvement

Disabling 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 new Input API

Swapping to the icon & inputClassName props simplifies markup and matches the refactored UI primitives. No issues spotted.


36-37: Nice UX detail

Moving 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 check

Reducing .btn-add-input-list height from h-8 to h-6 affects 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: Missing headers in new-server payload helpers

The accompanying hooks (useAddMCPServer, usePatchMCPServer) selectively serialise fields, but headers is 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 that configJsonLinux could be undefined, 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: handleOnNewValue prop leaked to child that doesn’t accept it.

ListSelectionComponent’s prop list does not include handleOnNewValue, yet it’s passed down, causing an implicit any prop. Remove it unless intentionally used by a spread.

src/frontend/tests/extended/features/mcp-server.spec.ts (1)

32-184: Well-structured test refactoring

The 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 utility

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

Comment on lines +6 to +18
# 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,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
# 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.

Comment on lines +84 to 88
// TODO: Remove this on cleanup
if (!ENABLE_DATASTAX_LANGFLOW) {
const langflowItems = CustomStoreSidebar();

const langflowItems = CustomStoreSidebar(true, ENABLE_LANGFLOW_STORE);
sidebarNavItems.splice(2, 0, ...langflowItems);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
// 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.

Comment on lines +9 to +11
# 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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
# 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.

Comment on lines 82 to 88
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

isEmptyFolder no longer respects current flowType – breaks component view

Switching the condition to

ENABLE_MCP ? flow.is_component === false : true

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

Comment on lines +15 to +25
const responseFn = async () => {
try {
const { data } = await api.get<getMCPServersResponse>(
`${getURL("MCP_SERVERS", undefined, true)}`,
);
return data;
} catch (error) {
console.error(error);
return [];
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +35 to +40
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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 = []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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 = None

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

Comment on lines 365 to 367
if self.tool != "":
exec_tool = self._tool_cache[self.tool]
tool_args = self.get_inputs_for_all_tools(self.tools)[self.tool]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +145 to +149
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 py

Length 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.py

Length 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_list instead 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.

Comment on lines +170 to +179
# 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
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants