Last validated: Langflow 1.12.x (nightly 1.12.0.dev9)
Covers the Add MCP Server modal as the registration surface for external MCP servers, across both transports, plus the security contract the stdio form now enforces (QA-CHECKLIST §14.1):
- stdio registration round-trip — a server registered with
command+argsresolves its tools into the MCPTools node'sdropdown_str_tool, renders the selected tool's inputs on the node, appears in Settings → MCP Servers, and can be edited and deleted from there. - Field persistence — every stdio field (name, command, N args, N env pairs) and every HTTP/SSE field (name, URL, N headers, N env pairs) survives save → reopen-for-edit.
- Tool-list refresh on edit — changing which package a registered server runs makes the node's tool list reflect the new server, not the cached one.
- The stdio command/args contract —
commandmust be a single executable; an option or package glued onto it is refused, and the same registration split intocommand+argsis accepted. - Streamable HTTP against Langflow itself — a project's own
/api/v1/mcp/project/{id}/streamableendpoint registers as an MCP server and exposes the project's flows as tools.
If this fails, external MCP servers can no longer be registered from the UI, the modal loses field state, the tool list serves stale data after an edit, or the stdio input-shape validation that keeps every policy layer seeing the same argv has been dropped.
@release @workspace @components @mcp @stable
(plus @regression on the command/args contract test)
@stable— promoted under #1091 after the file was brought back to green on nightly1.12.0.dev9with repeated--workers=1 --retries=0runs and a per-test force-failure check. Before #1091 the file carried no@stableand therefore ran in no automated lane, which is why every stdio registration in it had been broken since 2026-07-15 without a single red run.@regression— on the contract test only: it guards an intentional upstream security change (see External dependencies), so a silent removal of that validation must fail the suite.@workspace/@components— drives the flow canvas, sidebar and MCPTools node;@mcp— MCP server area;@release— happy-path MCP registration.
- Langflow running at
PLAYWRIGHT_BASE_URL; auto-login superuser. - The default MCP starter project exists (
lf-starter_project). - Network egress to the npm registry — the stdio tests run real MCP servers
via
npx. A cold container downloads the package on first use; see the timeout budgets below. npxonPATHinside the Langflow container (it ships there; verified on the nightly image).- No LLM / provider key required — no agent executes.
- Bootstrap; blank flow; add the first MCP component from the sidebar.
- Assert the node's
icon-Mcppaths use the theme-correct fill. - Open the Add MCP Server modal → stdio tab; register
command: npx,args[0]: @modelcontextprotocol/server-everythingunder a per-run random name. - Poll
GET /api/v2/mcp/servers?action_count=trueuntil the server'stoolsCountis non-null; opendropdown_str_tooland pickecho-0-option. - Assert the
echotool'smessageinput renders on the node. - Settings → MCP Servers → Edit the server: assert the JSON and HTTP tabs are
disabled, stdio is enabled, and both
stdio-command-input(npx) andstdio-args_0(the package) round-tripped. - Delete the server; assert it disappears from the list.
- Bootstrap; blank flow; open the MCP sidebar and its add-server trigger.
- Register the same
npx+server-everythingsplit under a random name. - Add the server's component to the canvas; assert
dropdown_str_toolrenders. - Settings → MCP Servers: assert the server is listed, delete it, assert it is gone.
- Bootstrap; add the
lf-starter_projectMCP component. - Open the modal → stdio tab; fill name,
command: uvx, and four args —mcp-server-test,--verbose,--port=8080,--config=test.json— plus two env pairs. (mcp-server-testis a deliberately non-existent package: this test asserts form persistence, not connectivity, and registration is accepted independently of whether the subprocess starts.) - Save; Settings → MCP Servers → Edit.
- Assert every field round-tripped: name, command,
stdio-args_0..3, and both env key/value pairs. - Escape the modal and delete the server.
Unchanged by #1091 (no stdio surface). Registers an HTTP server with two headers and two env pairs, reopens it for edit, and asserts all ten field values round-tripped; then deletes it.
- Bootstrap; add the
lf-starter_projectMCP component;adjustScreenViewand assert the canvas-controls menu is closed (zoom_outhidden) — the postcondition gate kept from #1053/#997. - Register server A:
command: npx,args[0]: @modelcontextprotocol/server-sequential-thinking. - Assert
dropdown_str_toolenables and exposessequentialthinking-0-option; select it and assert the tool's own inputs render on the node (anchor-popover-anchor-input-thoughtandint_int_thoughtnumber). - Settings → MCP Servers → Edit: assert
commandisnpxandargs[0]is the sequential-thinking package, then editargs[0]to@modelcontextprotocol/server-everything(server B) and save. - Return to the flow by id (
openFlowById), re-select the server on the node, and assert the tool list now exposesecho-0-option— the refresh, not the cached A list. - Delete the server; assert it is gone; re-register it as A again, return to
the flow by id, and assert the node's tool list is back to
sequentialthinking-0-option.
Both re-opens address the flow by id, never by the card whose name contains "New Flow" (#1340) — see the note below.
Unchanged by #1091 (no stdio surface). Derives the project's own
/api/v1/mcp/project/{id}/streamable URL, registers it via the HTTP tab, polls
toolsCount, and asserts ≥1 tool option; cleans up via the API.
- Bootstrap; blank flow; open the Add MCP Server modal → stdio tab.
- Fill a random name and
command: npx @modelcontextprotocol/server-everything(executable and package glued together); save. - Assert the rejection: the modal stays open (
add-mcp-server-buttonstill visible), an in-dialogrole="alert"carries/single executable name or path/, andGET /api/v2/mcp/serversdoes not list the name. - Without closing the modal, correct the input —
command: npx,args[0]: @modelcontextprotocol/server-everything— and save again. - Assert the acceptance: the modal closes, no alert remains, and the API now
lists the server with
command === "npx"andargs === ["@modelcontextprotocol/server-everything"]. - Delete the server via the API.
- stdio registration works only in the split shape. A
commandcarrying an embedded argument is refused with an in-dialog alert matching/single executable name or path/and creates no server; the same registration ascommand+args[0]is accepted, closes the modal, and is readable back fromGET /api/v2/mcp/serverswith exactly that command/args pair. - Tools resolve from a really-running server.
dropdown_str_toolexposes the tool testid the registered package actually serves (echo-0-optionforserver-everything,sequentialthinking-0-optionforserver-sequential-thinking) — not merely "some option". - The selected tool's own inputs render:
popover-anchor-input-message(echo),anchor-popover-anchor-input-thought+int_int_thoughtnumber(sequentialthinking). - Every modal field round-trips save → edit: stdio name/command/
args_0..3- 2 env pairs; HTTP name/URL + 2 headers + 2 env pairs.
- The tool list refreshes on edit: after changing
args[0]from sequential-thinking to server-everything, the node exposesecho-0-option; after reverting,sequentialthinking-0-option.
- Per-run random server names (
test_server_<5-digit>) — no test can pass on a server a previous run left behind. - Tool-name-specific option testids, never
[data-testid*="-option"]on the stdio path: a server that starts but serves the wrong tool set fails. This is exactly what the tool-refresh test turns into its assertion. - The contract test asserts both directions in one test. A refusal assert alone would still pass if the modal rejected everything; the accepted-shape half proves the validation is discriminating, not blanket.
- The contract test checks the API, not only the UI — a modal that stays open while the server is created anyway would pass a UI-only assert.
- Canvas-controls postcondition gate (
zoom_outhidden) in test 5 fails at the canvas controls instead of ~60 lines later as<html> intercepts pointer events(#576/#997/#1053). - Force-failure check (CONTRIBUTING §2) executed per test during VERIFY.
- MCP tool execution through a registered client server — covered by
mcp/client/mcp-client-regression.spec.tsandmcp/client/mcp-client-agent.spec.ts. - The MCP Server tab on a flow (exposing a project) —
mcp-server-tab.spec.ts. - Protocol-level tool listing/execution —
mcp-server-protocol.spec.ts. - Flow-file resources —
mcp-server-resources.spec.ts. - Registration status codes (409/404) —
mcp/client/mcp-server-registration-status-codes.spec.ts. - The rest of the stdio security policy — the arg blocklist
(
DANGEROUS_KEYWORDS), shell-metacharacter rejection, the docker-arg policy and the env blocklist are not covered here; test 7 covers only the command-shape rule. uvx-launched MCP servers that actually start. See Notes.
- Langflow's stdio security policy —
src/lfx/src/lfx/base/mcp/security.py→validate_mcp_stdio_config(). Since upstreamf4d6ac4(PR#14073, 2026-07-15, forward-porting the release-1.10.3 multi-tenant hardening from#13530/#14044),commandmust be a single executable name or path; options and arguments belong inargs.npxanduvxremain inALLOWED_MCP_COMMANDS. This is the contract test 7 pins. - Public npm registry —
@modelcontextprotocol/server-everythingand@modelcontextprotocol/server-sequential-thinkingare fetched bynpxinside the Langflow container. - Add-MCP-server modal testids (
stdio-tab,stdio-name-input,stdio-command-input,stdio-args_N,input-list-plus-btn_-0,stdio-env-key-N/stdio-env-value-N,stdio-env-plus-btn-0,http-tab,http-name-input,http-url-input,http-headers-*,http-env-*,add-mcp-server-button) andhelpers/mcp/open-add-mcp-server-modal.ts. - Settings → MCP Servers page (
sidebar-nav-MCP Servers,add-mcp-server-button-page,mcp-server-menu-button-<name>,btn_delete_delete_confirmation_modal). - MCPTools node (
dropdown_str_tool,mcp-server-dropdown,list_item_<name>). GET/DELETE /api/v2/mcp/servers[/{name}],helpers/auth/get-auth-token.ts,helpers/other/await-bootstrap-test.ts,helpers/ui/adjust-screen-view.ts,helpers/ui/zoom-out.ts,helpers/flows/delete-flow.ts,helpers/flows/add-component-from-sidebar.ts(addComponentFromSidebarWithoutSearch).
- If
validate_mcp_stdio_config()changes which command shapes are accepted, or the rejection message stops matching/single executable name or path/. - If the add-server modal testids or the args/env list controls change.
- If either
@modelcontextprotocolpackage renames its tools (echo,sequentialthinking) or stops publishing. - If the MCPTools node's tool-input testid derivation changes (integers are
lowercased into
int_int_<name>; strings keep their case inpopover-anchor-input-<name>).
-
#1340 — test 5 re-opened a flow by NAME, and it opened the wrong one. Both re-opens clicked the first
list-cardwhose name contained "New Flow". Langflow names every blank flow "New Flow"/"New Flow (N)", so underfullyParallelthe shared project holds one per worker and.first()resolves whichever the list puts first. Measured on nightly1.12.0.dev18: in isolation the test's own flow ranks first and the click is correct (which is why this never appeared in the daily history — no recorded failure on this test), but seeding one competingNew Flow …in the same project before the list fetch is enough to flip it — the rendered order became["New Flow probeB-…", "New Flow (1)", "Basic Prompting"], the click opened the competitor, and the test then died on thetext="MCP Tools"wait at 30 s, blaming the node for a flow it was never in. The same locator, inauto-save-off.spec.ts, cost two dailies before it was diagnosed (#1336). Both re-opens now useopenFlowById(#1214), the repo's by-id entry, which also seeds the assistant-onboarding flag and gates on the flow being writable — neither of which the card click did (#1005). The flow id is read AFTER the blank-flow navigation, never before it: the bootstrap parks the page on a placeholder flow Langflow deletes as soon as the modal navigates elsewhere (#490/#681). -
Pre-existing flake, NOT introduced by #1340:
openAddMcpServerModal. This test fails roughly 1 run in 3 locally athelpers/mcp/open-add-mcp-server-modal.ts:10(mcp-server-dropdown,locator.click: Timeout 3000ms exceeded) — the #1335 signature, in a second file. Confirmed by a control run of the unmodified spec: same 2/3, same step. Raising that budget to 30 s locally did not help under--workers=2+, where the dropdown simply never becomes clickable; a 4-worker burst of this spec fails 3/4 there, always before the re-open. That budget belongs to #1335 and is deliberately untouched here — it is a shared MCP helper with other callers. -
Why
npxand notuvxfor the servers that must really start. Before #1091 tests 1/2/5 registereduvx mcp-server-fetch/mcp-server-time. Splitting those intocommand+argsgets past the new validation but the subprocess still dies: the publishedmcp-server-fetchandmcp-server-timepackages fail at import against the currentmcpPython SDK —ImportError: cannot import name 'McpError' from 'mcp.shared.exceptions'(renamed toMCPError), reproduced inside the nightly container and not fixed by pinning the server version, because themcpdependency floats. That is a third-party breakage inmodelcontextprotocol/servers, outside both Langflow and this suite. Thenpxservers start cleanly on the same image and are already the shape the@stablemcp/client/specs use, so tests 1/2/5 register throughnpx.uvxstays covered as a command by test 3, which only asserts form persistence. -
Timeout budgets.
npxcold-starts a package download on a fresh container. The sibling stdio test inmcp-client-regression.spec.tswas raised to 120 s for exactly this (#463), so the tool-list waits here use the same 120 s budget rather than the 30 s the file carried while it was never running in CI. The subsequent option/testid waits stay short (10 s) — oncetoolsCountis non-null the dropdown is local state. Test 5 carries three of those 120 s waits (register A → edit to B → re-register A), which does not fit the suite's 5-minute per-test cap, so it raises its own budget to 8 min viatest.setTimeout— otherwise a slow registry surfaces as a test timeout instead of as the wait that actually ran out. -
A second defect the fix exposed. With registration working again, test 1 reached an assertion it had never executed: it sampled the selected tool's
messageinput with a barecount()immediately after clicking the option. The node's inputs arrive with a rebuild a beat later, so the count was 0. It is now an auto-retryingtoBeVisible, matching how the@stablesibling that selects the sameechotool waits (mcp-client-regression.spec.ts) — which is why that spec never hit the race and this one could not have, while its registration was failing 60 lines earlier. -
Flow cleanup. Every test bootstraps and creates a flow. Ids are collected from
POST /api/v1/flows201 responses (pattern A —awaitBootstrapTestruns first, so the canvas URL id is not trustworthy, #681) and deleted id-scoped inafterEach. Registered MCP servers are also deleted by name inafterEach, so a mid-test failure cannot leak one into the next run. -
Trace-on may hang on this ReactFlow-canvas family (see the skill's known
--trace=onlimitation); step verification relies on--retries=0bursts + force-fail. -
A commented-out seventh block (SSE against a public Cloudflare MCP endpoint) remains at the bottom of the file, untouched by #1091.
-
The three MCP-starter adds are repaired, not bare clicks (#1335). Langflow swallows that sidebar click on the MCP tab roughly half the time on nightly 1.12.0.dev17 (measured 4/8, all 4 repaired by an identical second click), and every entry point of the add-server modal hangs off the node it should have created. Measured locally on dev17 before and after: this file failed 3 of its 6 runnable tests with the bare clicks — including the
@stabletests 3 ("STDIO … fields should persist") and 5 ("tools should be refreshed …") — and 1 of 6 withaddComponentFromSidebarWithoutSearch. The remaining failure is test 6 ("Streamable HTTP … server-everything"), which registers through the sidebar page rather than the modal, fails identically with and without this change, and is not@stable.