fix: MCP Tools component now works in LFX standalone mode#12760
fix: MCP Tools component now works in LFX standalone mode#12760erichare merged 12 commits intorelease-1.10.0from
Conversation
When serving a flow via `lfx` (the standalone flow server) without the full Langflow package installed, `MCPToolsComponent.update_tool_list()` unconditionally tried to import `langflow.api.v2.mcp.get_server` and open a DB session, raising `ImportError` before the existing fallback to `server_config_from_value` in `resolve_mcp_config()` could run. Treat the ImportError as the expected signal of standalone mode: skip the DB lookup and use the server config embedded in the flow JSON. The DB path is unchanged when Langflow is available.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Previously the fallback to the flow-embedded MCP server config triggered on any ImportError from the Langflow imports, which would silently swallow transitive dependency failures (e.g. sqlmodel failing to import inside langflow.services.database.models.user.crud). That is a real bug in the full Langflow stack — not LFX standalone mode — and silently using the stale flow-embedded config would hide it while also bypassing the DB config that is supposed to take precedence when Langflow is available. Narrow the fallback to ModuleNotFoundError whose .name is one of the Langflow modules we are trying to import; re-raise everything else so it surfaces normally.
Codecov Report✅ All modified and coverable lines are covered by tests. ❌ Your project status has failed because the head coverage (49.78%) is below the target coverage (60.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## release-1.10.0 #12760 +/- ##
==================================================
- Coverage 52.81% 52.78% -0.03%
==================================================
Files 2026 2026
Lines 183818 184528 +710
Branches 27677 28920 +1243
==================================================
+ Hits 97083 97409 +326
- Misses 85636 86025 +389
+ Partials 1099 1094 -5
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Cristhianzl
left a comment
There was a problem hiding this comment.
🧭 TL;DR Verdict
| Dimension | Result |
|---|---|
| 🔴 CRITICAL blockers | None |
| 🟠 IMPORTANT (must-fix before merge) | 1 |
| 🟡 RECOMMENDED | 4 |
| 🟢 NICE TO HAVE | 2 |
| Overall | Approve with minor changes — a small, surgical fix; the second commit demonstrates exemplary security-mindset by distinguishing "Langflow absent" (expected fallback) from "Langflow broken" (real bug that must surface) |
The fix is narrow and on-target. The progression of commits is the kind of collaboration you want to see: commit 1 is the obvious fix (except ImportError); commit 3 is the security-mindful refinement (except ModuleNotFoundError + CAS on e.name) that prevents a silent-fallback-on-real-bug failure mode. Both scenarios are covered by explicit regression tests.
🔴 CRITICAL — Security & PII Blockers
None found.
Verifications performed:
- No
print(...), no PII in any log call. The only new log statement islogger.adebug("Langflow package not available; using MCP server config from flow value (LFX standalone mode).")— zero user data. - No secrets, tokens, or credentials are logged, stored, or returned in error messages. The
server_configdict is passed through internally but never logged. - No
eval,exec,os.system, SQL string concat, subprocess injection, or XSS surface. - No changes to authentication/authorization paths. The DB path remains identical when Langflow is available; only the import-failure branch is changed.
- The test uses
test_user_123(literal test identifier) andhttp://…does not appear — no real endpoints.
Trust boundary analysis: the new code trusts getattr(e, "name", None) — ModuleNotFoundError.name is populated by the Python import system itself (CPython ≥ 3.3), not by user input, so it is authoritative and not spoofable from outside the process.
🟠 IMPORTANT — Must Fix Before Merge
1. The intentional narrowing from ImportError → ModuleNotFoundError is load-bearing but under-documented
File: src/lfx/src/lfx/components/models_and_agents/mcp_component.py:242
except ModuleNotFoundError as e:
# Only treat this as LFX standalone mode when one of the target Langflow
# modules is itself missing. Transitive ModuleNotFoundError (e.g. a
# dependency like sqlmodel failing to import inside langflow.*) indicates
# a real bug in the full Langflow stack and must surface — otherwise we
# would silently use a stale flow-embedded config when DB config should
# have taken precedence.
missing_module = getattr(e, "name", None) or ""
is_langflow_standalone = missing_module == "langflow" or missing_module.startswith("langflow.")
if not is_langflow_standalone:
raiseThe previous code caught ImportError (which is the parent class of ModuleNotFoundError). The new code catches only ModuleNotFoundError. That tightening is correct but silent — a future maintainer who "cleans up" by broadening back to except ImportError would reintroduce a bug:
ModuleNotFoundError— raised when a module cannot be located (import langflowwhen langflow is absent).- Plain
ImportError(not aModuleNotFoundError) — raised when the module exists but the attribute being imported does not (from langflow.api.v2.mcp import get_serverwhen Langflow is installed butget_serverwas renamed/removed). That case should surface as a bug, not be silently treated as standalone mode.
The current comment explains the transitive case but not the ImportError-vs-ModuleNotFoundError distinction. This matters because REVIEWER_RULE.md §Error Handling and DEVELOPMENT_RULE.md §Error Handling both require error handling to be part of the API contract.
- Fix: expand the comment to make the narrowing explicit. One extra line is enough:
# Deliberately `except ModuleNotFoundError` (not `except ImportError`): a plain # ImportError here means `get_server` / `get_user_by_id` was removed from an # installed Langflow — a real API break that should NOT be swallowed as # "standalone mode". ModuleNotFoundError alone covers the "Langflow absent" case.
- Bonus: add a third regression test to pin this behavior:
async def test_attribute_import_error_surfaces(self, component): """Plain ImportError (attribute missing, not module missing) must surface.""" # Patch the langflow module so the module imports but get_server raises ImportError. # Assert ValueError("Error updating tool list") is raised, not a silent fallback.
🟡 RECOMMENDED — Should Fix
2. Log level for the fallback path
File: src/lfx/src/lfx/components/models_and_agents/mcp_component.py:253
await logger.adebug(
"Langflow package not available; using MCP server config from flow value (LFX standalone mode)."
)- Why:
DEVELOPMENT_RULE.md §Observability— "Appropriate log levels (debug, info, warn, error)". This message describes a mode switch that materially changes the resolved config (DB path skipped, flow-embedded config used). Developers debugging LFX standalone deployments will run withinfo, notdebug, and currently have no signal that the fallback fired. - Fix: bump to
await logger.ainfo(...). This is the one-off mode-detection log — not noisy. The per-request DB-path logs (not touched by this PR) can stay at debug.
3. Edge case not covered: lfx.services.deps itself missing
File: src/lfx/src/lfx/components/models_and_agents/mcp_component.py:241
The inner try imports three modules, one of which is from lfx.services.deps import get_settings_service. The current prefix check is missing_module == "langflow" or missing_module.startswith("langflow."). If lfx.services.deps is the missing module (a packaging error inside lfx itself), the check correctly returns False and re-raises — good. But there is no test pinning this behavior.
- Why:
REVIEWER_RULE.md §🔴 Tests MUST Also Challenge the CodeandDEVELOPMENT_RULE.md §Testing – Adversarial tests. - Fix: add a third regression test that raises
ModuleNotFoundError(name="lfx.services.deps")and asserts the outerValueError("Error updating tool list")wraps it. This locks in the intended precedence rule in case someone later rewrites the prefix check.
4. Flag the legacy file-size debt
File: src/lfx/src/lfx/components/models_and_agents/mcp_component.py — 793 lines.
Way over the 500-line hard limit in REVIEWER_RULE.md §File Structure Limits. The PR did not create the problem — it adds only ~11 net lines — so this is not a blocker, but §Legacy Code Awareness requires "Flag legacy patterns you encounter for future cleanup".
- Recommended: open a follow-up ticket to split
MCPToolsComponent's helper functions (resolve_mcp_config, connection resolution) from the orchestration logic. No action required in this PR beyond a# TODO(<ticket>):marker at the top of the class.
5. patch("builtins.__import__") is a heavyweight test strategy
File: src/backend/tests/unit/components/models_and_agents/test_mcp_component.py:226, 262
with patch("builtins.__import__", side_effect=fake_import):
...- Why: swapping Python's import machinery affects every import that happens while the patch is active — including unrelated library code pulled in by
await, mocks, assertions, etc. Thereal_importfallback mitigates this but also fragile — a change elsewhere in the test's call chain could start trippinglangflow_prefixesunexpectedly. - Mitigation: these tests are specifically about import failures, so some approach like this is unavoidable — but a comment on why patching
builtins.__import__is necessary (instead of the more targetedsys.modules/importlib) would help future maintainers choose correctly. And a belt-and-braces guard that the fake is scoped as tightly as possible.
🟢 NICE TO HAVE — Polish
6. Simplify getattr(e, "name", None) or ""
File: mcp_component.py:249
ModuleNotFoundError.name is guaranteed set by the import system for exceptions Python itself raises. getattr(..., None) is defensive but pairs awkwardly with or "". A slightly cleaner spelling:
missing_module = e.name or ""Equivalent and more direct. Keep the current form if you're worried about manually-instantiated ModuleNotFoundError() with no name=; both are correct. Pure taste.
7. Test name precision
test_transitive_import_error_surfaces — clear but doesn't mention the caller. Per the should_[expected]_when_[condition] convention in REVIEWER_RULE.md §Test Quality, something like test_update_tool_list_surfaces_transitive_import_error_instead_of_falling_back would be more self-describing when grep'd in CI output.
✅ Compliant Aspects Worth Highlighting
- Exemplary two-step error handling: commit 1 catches the broad case; commit 3 narrows to the precise signal (
missing_module == "langflow" or missing_module.startswith("langflow.")). This isDEVELOPMENT_RULE.md §Security Mindsetin action — the author explicitly named and fixed an assumption ("any ImportError means standalone") that would have produced a silent wrong-answer failure. - Error chain preserved: the outer handler's
raise ValueError("Error updating tool list: ...") from eis unchanged, so__cause__still carries the originalModuleNotFoundErrorfor the caller. The second regression test explicitly assertsexc_info.value.__cause__.name == "sqlmodel". Excellent. - Both happy and adversarial paths covered:
- Happy:
test_lfx_standalone_falls_back_to_value_config—update_toolscalled with the expected embedded config. - Adversarial:
test_transitive_import_error_surfaces—update_toolsnot called; the outerValueErroris raised with the correct__cause__.
- Happy:
raisewithout arguments: inside theexceptbranch, the bareraisepreserves the original traceback.DEVELOPMENT_RULE.md §Error Handlingcompliant.- Comments explain WHY, not WHAT: the inline comment at
util.py:243-248documents the transitive-dep failure mode with a concrete example (sqlmodel). No restating-the-code cruft. - No PII, no secrets, no eval, no
innerHTML, no SQL concat. - Generated artifacts refreshed:
component_index.jsonand theNvidia Remix.jsonstarter project have updatedcode_hashand embedded source — the normal Langflow release workflow for code changes inside bundled components. Not a review concern.
🧪 Security Mindset — The Five Questions (per REVIEWER_RULE.md and DEVELOPMENT_RULE.md §Security Mindset)
| # | Question | Answer for this PR |
|---|---|---|
| 1 | What is this code trusting without verifying? | e.name on a ModuleNotFoundError. CPython sets it for every import-system-raised exception; only manually constructed errors might omit it. The or "" fallback maps the missing case to "surface the error", which is the safe default. ✅ |
| 2 | Authoritative source for behavior? | ModuleNotFoundError.name is the Python 3.3+ standard-library contract. ✅ |
| 3 | What happens in every failure path? | Plain ImportError (attribute missing) and transitive ModuleNotFoundError both propagate to the outer except Exception and get wrapped as ValueError("Error updating tool list: ...") with __cause__ preserved. langflow.* absence falls through to server_config_from_value. Both are tested. ✅ |
| 4 | Who controls each value, and can they lie? | All values here are internal to the Python runtime / imports. No external control surface. ✅ |
| 5 | Blast radius if wrong? | Pre-fix: MCP Tools component always failed in LFX standalone. Post-fix: it works when Langflow is absent; real bugs (attribute imports, transitive deps) still surface. Net: strictly smaller blast radius. ✅ |
📋 Final Pre-Merge Checklist
🔴 CRITICAL PASS
🟠 IMPORTANT 1 item — resolve before merge
[ ] 1. Add a one-line comment explaining the `ImportError` → `ModuleNotFoundError`
narrowing; optionally add a regression test for the plain-ImportError case.
🟡 RECOMMENDED 4 items — strongly encouraged
[ ] 2. Bump fallback log from `adebug` to `ainfo`
[ ] 3. Add edge-case test for `lfx.services.deps` itself missing
[ ] 4. Add TODO(<ticket>) marker flagging the 793-line file debt
[ ] 5. Explain why `patch("builtins.__import__")` is necessary
🟢 NICE TO HAVE 2 items — polish
[ ] 6. Simplify `getattr(e, "name", None) or ""` to `e.name or ""`
[ ] 7. More precise test name (should_[expected]_when_[condition])
Testing
[x] Happy-path regression test (standalone fallback) — added
[x] Adversarial test (transitive ImportError surfaces) — added
[x] Error-chain assertion (__cause__ preserved) — added
[x] 4 existing tests still pass (confirmed in PR description)
[ ] Show pytest coverage output for the touched function (≥ 75%, target 80%)
[ ] Manual repro on real LFX standalone run (PR description has this un-ticked)
- Expand comment on `except ModuleNotFoundError` narrowing to explicitly
document the ImportError-vs-ModuleNotFoundError distinction.
- Bump fallback log from adebug to ainfo (mode switch is material).
- Simplify `getattr(e, "name", None) or ""` to `e.name or ""`.
- Add TODO(legacy-cleanup) marker flagging 800-line file debt.
- Rename fallback tests to `should_[expected]_when_[condition]` convention.
- Add regression tests: plain ImportError surfaces; lfx.services.deps
missing surfaces (locks in prefix-check precedence).
- Document why `patch("builtins.__import__")` is necessary in tests.
|
Pushed as Thanks for the thorough review! Pushed 5e03b50 addressing all 1 🟠 + 4 🟡 + 2 🟢 items:
All 8 tests in |
Resolve conflicts in MCPToolsComponent: keep the release-1.10.0 concurrency lock and header-aware cache keys while preserving this branch's ModuleNotFoundError fallback so update_tool_list still works when the full Langflow package is absent (LFX standalone mode). Regenerate component_index.json and Nvidia Remix starter project so the embedded code hash reflects the merged source.
* fix: MCP Tools component now works in LFX standalone mode
When serving a flow via `lfx` (the standalone flow server) without the
full Langflow package installed, `MCPToolsComponent.update_tool_list()`
unconditionally tried to import `langflow.api.v2.mcp.get_server` and
open a DB session, raising `ImportError` before the existing fallback
to `server_config_from_value` in `resolve_mcp_config()` could run.
Treat the ImportError as the expected signal of standalone mode: skip
the DB lookup and use the server config embedded in the flow JSON.
The DB path is unchanged when Langflow is available.
* [autofix.ci] apply automated fixes
* fix: narrow LFX standalone fallback to missing Langflow modules only
Previously the fallback to the flow-embedded MCP server config triggered
on any ImportError from the Langflow imports, which would silently swallow
transitive dependency failures (e.g. sqlmodel failing to import inside
langflow.services.database.models.user.crud). That is a real bug in the
full Langflow stack — not LFX standalone mode — and silently using the
stale flow-embedded config would hide it while also bypassing the DB
config that is supposed to take precedence when Langflow is available.
Narrow the fallback to ModuleNotFoundError whose .name is one of the
Langflow modules we are trying to import; re-raise everything else so it
surfaces normally.
* Update component_index.json
* [autofix.ci] apply automated fixes
* fix: address review feedback on LFX standalone fallback
- Expand comment on `except ModuleNotFoundError` narrowing to explicitly
document the ImportError-vs-ModuleNotFoundError distinction.
- Bump fallback log from adebug to ainfo (mode switch is material).
- Simplify `getattr(e, "name", None) or ""` to `e.name or ""`.
- Add TODO(legacy-cleanup) marker flagging 800-line file debt.
- Rename fallback tests to `should_[expected]_when_[condition]` convention.
- Add regression tests: plain ImportError surfaces; lfx.services.deps
missing surfaces (locks in prefix-check precedence).
- Document why `patch("builtins.__import__")` is necessary in tests.
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes (attempt 2/3)
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes (attempt 2/3)
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Fixes #12759
Summary
lfx(the standalone flow server),MCPToolsComponent.update_tool_list()unconditionally importedlangflow.api.v2.mcp.get_serverand opened a database session viasession_scope(). In LFX standalone mode neither the full Langflow package nor a database is available, so anImportErrorwas raised before the existing fallback toserver_config_from_valueinresolve_mcp_config()could run.ImportErroras the expected signal of LFX standalone mode: skip the DB lookup and use the server config embedded in the flow JSON. The DB path is unchanged when Langflow is available, and DB config still takes priority when present.test_lfx_standalone_falls_back_to_value_config) that patchesbuiltins.__import__to raiseImportErrorforlangflow.api.v2.mcp/langflow.services.database.*and assertsupdate_tool_list()completes successfully using the embedded config.Root cause
src/lfx/src/lfx/components/models_and_agents/mcp_component.pyBefore: the inner
try/except ImportErrorre-raisedImportError, which was caught by the outerexcept Exceptionand re-raised asValueError(\"Error updating tool list: ...\"). The fallback toserver_config_from_valueinresolve_mcp_config()was never reached.After: the inner
except ImportErrorlogs a debug message and falls through;server_config_from_dbstaysNone;resolve_mcp_config()resolves toserver_config_from_value;update_tools(...)runs with the embedded config.Impact
lfxstandalone mode when the server config is embedded in the flow JSON.Test plan
pytest src/backend/tests/unit/components/models_and_agents/test_mcp_component.py::TestMCPComponentConfigPriority— all 5 tests pass (4 existing + 1 new regression test).lfx, invoke MCP Tools — component runs instead of raisingImportError/ValueError.test_database_config_takes_priority_over_value/test_database_config_used_when_no_value_config).