From 7f6721edd2b9615f800580f86556b8059b84448d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 7 Oct 2025 17:11:27 -0300 Subject: [PATCH 1/3] feat: Add mcp_composer_version field with validation in Settings class - Introduced mcp_composer_version attribute to specify version constraints for mcp-composer using PEP 440 syntax. - Implemented a field validator to ensure version strings have appropriate specifier prefixes, defaulting to '~=0.1.0.7' if none is provided. - Enhanced documentation for clarity on versioning behavior and validation logic. --- src/lfx/src/lfx/services/settings/base.py | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/lfx/src/lfx/services/settings/base.py b/src/lfx/src/lfx/services/settings/base.py index c1557f1782e5..64326f404962 100644 --- a/src/lfx/src/lfx/services/settings/base.py +++ b/src/lfx/src/lfx/services/settings/base.py @@ -285,6 +285,9 @@ class Settings(BaseSettings): # MCP Composer mcp_composer_enabled: bool = True """If set to False, Langflow will not start the MCP Composer service.""" + mcp_composer_version: str = "~=0.1.0.7" + """Version constraint for mcp-composer when using uvx. Uses PEP 440 syntax. + ~=0.1.0.7 allows patch updates (0.1.0.x) but prevents minor/major version changes.""" # Public Flow Settings public_flow_cleanup_interval: int = Field(default=3600, gt=600) @@ -348,6 +351,34 @@ def set_user_agent(cls, value): logger.debug(f"Setting user agent to {value}") return value + @field_validator("mcp_composer_version", mode="before") + @classmethod + def validate_mcp_composer_version(cls, value): + """Ensure the version string has a version specifier prefix. + + If a bare version like '0.1.0.7' is provided, prepend '~=' to allow patch updates. + Supports PEP 440 specifiers: ==, !=, <=, >=, <, >, ~=, === + """ + if not value: + return "~=0.1.0.7" # Default + + # Check if it already has a version specifier + # Order matters: check longer specifiers first to avoid false matches + specifiers = ["===", "==", "!=", "<=", ">=", "~=", "<", ">"] + if any(value.startswith(spec) for spec in specifiers): + return value + + # If it's a bare version number, add ~= prefix + # This regex matches version numbers like 0.1.0.7, 1.2.3, etc. + import re + + if re.match(r"^\d+(\.\d+)*", value): + logger.debug(f"Adding ~= prefix to bare version '{value}' -> '~={value}'") + return f"~={value}" + + # If we can't determine, return as-is and let uvx handle it + return value + @field_validator("variables_to_get_from_environment", mode="before") @classmethod def set_variables_to_get_from_environment(cls, value): From 424c4b5f58eaa1d7bbf069f5dab71ebfe331267b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 7 Oct 2025 17:18:18 -0300 Subject: [PATCH 2/3] feat: Integrate mcp_composer_version into MCP Composer command construction - Updated the command for starting the MCP Composer subprocess to include the mcp_composer_version from settings, ensuring the correct version is used. - Enhanced the installation function to reflect the same change, improving consistency across the codebase. --- src/backend/base/langflow/api/v1/mcp_projects.py | 3 ++- src/lfx/src/lfx/services/mcp_composer/service.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backend/base/langflow/api/v1/mcp_projects.py b/src/backend/base/langflow/api/v1/mcp_projects.py index 793df020614e..74b69d8a661a 100644 --- a/src/backend/base/langflow/api/v1/mcp_projects.py +++ b/src/backend/base/langflow/api/v1/mcp_projects.py @@ -629,9 +629,10 @@ async def install_mcp_config( raise HTTPException(status_code=500, detail=error_detail) from e # For OAuth/MCP Composer, use the special format + settings = get_settings_service().settings command = "uvx" args = [ - "mcp-composer", + f"mcp-composer{settings.mcp_composer_version}", "--mode", "stdio", "--sse-url", diff --git a/src/lfx/src/lfx/services/mcp_composer/service.py b/src/lfx/src/lfx/services/mcp_composer/service.py index 105e592879a7..a48ef39cfb33 100644 --- a/src/lfx/src/lfx/services/mcp_composer/service.py +++ b/src/lfx/src/lfx/services/mcp_composer/service.py @@ -417,9 +417,10 @@ async def _start_project_composer_process( startup_delay: float = 2.0, ) -> subprocess.Popen: """Start the MCP Composer subprocess for a specific project.""" + settings = get_settings_service().settings cmd = [ "uvx", - "mcp-composer", + f"mcp-composer{settings.mcp_composer_version}", "--mode", "sse", "--sse-url", @@ -447,7 +448,7 @@ async def _start_project_composer_process( "oauth_server_url": "OAUTH_SERVER_URL", "oauth_callback_path": "OAUTH_CALLBACK_PATH", "oauth_client_id": "OAUTH_CLIENT_ID", - "oauth_client_secret": "OAUTH_CLIENT_SECRET", + "oauth_client_secret": "OAUTH_CLIENT_SECRET", # pragma: allowlist secret "oauth_auth_url": "OAUTH_AUTH_URL", "oauth_token_url": "OAUTH_TOKEN_URL", "oauth_mcp_scope": "OAUTH_MCP_SCOPE", From e8f0c8879330da3d8a91d287eb162536d139c436 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 7 Oct 2025 17:35:32 -0300 Subject: [PATCH 3/3] test: Add unit tests for mcp_composer_version validation in Settings - Introduced tests for the mcp_composer_version validator, covering various version formats and ensuring correct behavior for both valid and default cases. - Created a new test file for settings services, enhancing test coverage and documentation for the versioning logic. --- .../tests/unit/services/settings/__init__.py | 1 + .../settings/test_mcp_composer_version.py | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/lfx/tests/unit/services/settings/__init__.py create mode 100644 src/lfx/tests/unit/services/settings/test_mcp_composer_version.py diff --git a/src/lfx/tests/unit/services/settings/__init__.py b/src/lfx/tests/unit/services/settings/__init__.py new file mode 100644 index 000000000000..29f9d8b65d2e --- /dev/null +++ b/src/lfx/tests/unit/services/settings/__init__.py @@ -0,0 +1 @@ +"""Tests for settings services.""" diff --git a/src/lfx/tests/unit/services/settings/test_mcp_composer_version.py b/src/lfx/tests/unit/services/settings/test_mcp_composer_version.py new file mode 100644 index 000000000000..d3f57e35c0b9 --- /dev/null +++ b/src/lfx/tests/unit/services/settings/test_mcp_composer_version.py @@ -0,0 +1,120 @@ +"""Tests for mcp_composer_version validator in Settings.""" + +from lfx.services.settings.base import Settings + + +def test_bare_version_gets_tilde_equals_prefix(monkeypatch): + """Test that a bare version like '0.1.0.7' gets ~= prefix added.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == "~=0.1.0.7" + + +def test_version_with_tilde_equals_is_preserved(monkeypatch): + """Test that a version with ~= is preserved as-is.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "~=0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == "~=0.1.0.7" + + +def test_version_with_greater_than_or_equal_is_preserved(monkeypatch): + """Test that a version with >= is preserved as-is.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", ">=0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == ">=0.1.0.7" + + +def test_version_with_exact_match_is_preserved(monkeypatch): + """Test that a version with == is preserved as-is.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "==0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == "==0.1.0.7" + + +def test_version_with_less_than_or_equal_is_preserved(monkeypatch): + """Test that a version with <= is preserved as-is.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "<=0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == "<=0.1.0.7" + + +def test_version_with_not_equal_is_preserved(monkeypatch): + """Test that a version with != is preserved as-is.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "!=0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == "!=0.1.0.7" + + +def test_version_with_less_than_is_preserved(monkeypatch): + """Test that a version with < is preserved as-is.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "<0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == "<0.1.0.7" + + +def test_version_with_greater_than_is_preserved(monkeypatch): + """Test that a version with > is preserved as-is.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", ">0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == ">0.1.0.7" + + +def test_version_with_arbitrary_equality_is_preserved(monkeypatch): + """Test that a version with === is preserved as-is.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "===0.1.0.7") + settings = Settings() + assert settings.mcp_composer_version == "===0.1.0.7" + + +def test_empty_version_gets_default(monkeypatch): + """Test that empty string gets default value.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "") + settings = Settings() + assert settings.mcp_composer_version == "~=0.1.0.7" + + +def test_no_env_var_uses_default(monkeypatch): + """Test that missing env var uses default value.""" + monkeypatch.delenv("LANGFLOW_MCP_COMPOSER_VERSION", raising=False) + settings = Settings() + assert settings.mcp_composer_version == "~=0.1.0.7" + + +def test_three_part_version_gets_prefix(monkeypatch): + """Test that a 3-part version like '1.2.3' gets ~= prefix.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "1.2.3") + settings = Settings() + assert settings.mcp_composer_version == "~=1.2.3" + + +def test_two_part_version_gets_prefix(monkeypatch): + """Test that a 2-part version like '1.2' gets ~= prefix.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "1.2") + settings = Settings() + assert settings.mcp_composer_version == "~=1.2" + + +def test_single_digit_version_gets_prefix(monkeypatch): + """Test that a single digit version like '1' gets ~= prefix.""" + monkeypatch.setenv("LANGFLOW_MCP_COMPOSER_VERSION", "1") + settings = Settings() + assert settings.mcp_composer_version == "~=1" + + +def test_validator_directly(): + """Test calling the validator method directly.""" + # Test bare version + result = Settings.validate_mcp_composer_version("0.1.0.7") + assert result == "~=0.1.0.7" + + # Test with specifier + result = Settings.validate_mcp_composer_version(">=0.1.0.7") + assert result == ">=0.1.0.7" + + # Test empty + result = Settings.validate_mcp_composer_version("") + assert result == "~=0.1.0.7" + + # Test None + result = Settings.validate_mcp_composer_version(None) + assert result == "~=0.1.0.7"