Skip to content

Commit a2d3c18

Browse files
ogabrielluizautofix-ci[bot]keval718erichare
authored
feat: MCP server UX improvements, batch, and spec-based flow creation (#12205)
* feat: add pure flow-builder utilities to lfx Add flow_builder subpackage with pure functions for manipulating flow JSON dicts — component ops, edge creation with ReactFlow handle format, topological layout, and dynamic field detection. * feat: add MCP server for operating Langflow via REST API FastMCP server exposing 15 tools across auth, flow, component, connection, and execution groups. Agents can create flows, add and configure components, wire connections, and run flows against a Langflow server through MCP tool calls. * feat: add langflow-mcp-client console script entry point * fix: use dict comprehension in setup.py to fix PERF403 lint * fix: address PR review feedback on MCP client - Add asyncio.Lock to prevent race condition in _client() under concurrent access - Handle 204 No Content responses in delete() instead of calling resp.json() on empty body - Fix weak assertion in test_default_values * feat: auto-enable tool_mode when connecting component_as_tool describe_component_type now shows component_as_tool as an output for any component with tool_mode-capable outputs. When an agent connects via component_as_tool, tool_mode is auto-enabled — no extra step needed. * refactor: move MCP server from langflow-base to lfx The MCP server has no langflow dependencies — only httpx, mcp, and lfx.graph.flow_builder. Moving it to lfx.mcp makes it usable without installing langflow. Entry point: lfx-mcp. * feat: improve MCP server UX for agents - describe_component_type separates advanced fields from core ones - search_component_types accepts output_type filter - list_flows accepts query filter and includes ASCII graph repr - get_flow_info includes ASCII graph repr - add duplicate_flow tool - add list_starter_projects tool * feat: add use_starter_project tool and tests for new features - use_starter_project creates a flow from a starter template by name (starter projects aren't fetchable by ID via /flows/) - Tests for duplicate_flow, starter projects, graph repr, advanced fields, and output_type search * docs: improve MCP tool descriptions for agent clarity - Add server-level instructions with typical workflow guide - Remove internal implementation details from tool descriptions - Add cross-references between related tools - Mention component_as_tool and graph diagrams where relevant * docs: address subagent review feedback on tool descriptions - Document return values for create_flow, add_component - Clarify empty-query behavior for search_component_types - Distinguish get_component_info (instance) vs describe_component_type (type) - Explain connection type compatibility in instructions - Clarify configure_component trigger field behavior - State disconnect_components default when filters omitted * feat: add batch tool for multi-action requests Execute multiple actions in one call with $N.field references to chain results. An agent can build a complete flow in a single request instead of 6+ round trips. * feat: add create_flow_from_spec tool for compact text-based flow creation Accepts a compact text spec with nodes, edges (using real port names), and config sections. Agents generate a simple string instead of constructing nested JSON. Tool mode auto-enabled for component_as_tool. Handles Prompt Template dynamic variables by parsing {var} from template text and creating input fields. Cleans up flows on failure. Type coercion for numeric/boolean config values. * feat: add build_flow validation and create_flow_from_spec build_flow validates flows by building the graph server-side. create_flow_from_spec accepts a compact text spec with nodes, edges, and config. Validates by default (optional). Handles Prompt Template dynamic {variables}, auto-enables tool_mode for component_as_tool, cleans up on failure, coerces config types. * fix: isolate session state and harden MCP server * fix: move test_flow_builder into tests/unit so CI collects coverage * fix: address PR review feedback - Fix test fixture to use contextvars instead of stale module attributes - Raise ValueError on malformed spec lines instead of silently dropping - Disambiguate duplicate component types in flow_graph_repr - Narrow except Exception to ImportError in flow_graph_repr - Add action-index context to batch error messages - Fix stale/inaccurate docstrings (group count, "| ", field_name, category, build_flow) - Mention create_flow_from_spec in MCP instructions * feat: stream run_flow events via MCP progress notifications run_flow now consumes Langflow's SSE stream and relays token events to the MCP client via report_progress. Falls back to a regular POST if the stream yields no result. * test: add streaming integration tests for run_flow and stream_post * chore: rebuild component index * [autofix.ci] apply automated fixes * fix: handle Message dicts in str field param processing, add MCP logger param_handler's str case called unescape_string on list elements without type checking. On subsequent agent calls, chat history stores Message dicts in the list, causing 'dict' object has no attribute 'replace'. Added _coerce_str_value that extracts .text from Message/Data/dict objects. Added lfx logger to MCP server with streaming fallback warning. * feat: add flow builder tools, propose_field_edit, and flow_to_spec_summary - builder.py: builds flow dicts from text specs using local component registry with granular error handling per build phase - flow_builder_tools.py: 9 Langflow components for agent tooling (search, describe, get_field_value, propose_field_edit, add_component, remove_component, connect_components, configure_component, build_flow) - propose_field_edit generates validated JSON Patches with dry-run - flow_to_spec_summary converts flow dicts to compact summaries with IDs - Module-level event queue for real-time UI updates during streaming * [autofix.ci] apply automated fixes * feat: add get_build_results and get_component_output MCP tools Exposes per-component build data from the vertex_builds table: - get_build_results: returns all component outputs, validity, and errors from the last run -- useful for debugging which component failed - get_component_output: inspect a specific component's output from the last run to trace where the pipeline broke * feat: add flow management, iteration, and discovery MCP tools Response improvements: - spec_summary (component IDs + connection ports) in get_flow_info/list_flows - Merged components() tool: search or describe in one call Flow management tools: - validate_flow: polls build results with timeout, structured per-component errors - rename_flow: update name/description - export_flow: serialize to JSON with sensitive field redaction - update_flow_from_spec: declarative update with reference validation Component iteration tools: - freeze_component / unfreeze_component: skip re-execution during iteration - layout_flow_tool: re-layout after modifications Security: export_flow redacts API keys via redact_node before exposing to LLM. Includes 18 integration tests covering all new tools. * refactor: extract shared _node_id and validate_spec_references - _utils.py: shared node_id helper (was duplicated in component.py and layout.py) - spec.py: validate_spec_references extracted from three copies in create_flow_from_spec, update_flow_from_spec, and build_flow_from_spec * fix: add trailing slash to /api_key endpoint in MCP client login The FastAPI endpoint redirects /api_key to /api_key/ (307) and httpx drops the POST body on redirect, causing API key creation to fail silently during login. * fix: isolate session state and harden MCP server contextvars alone lose state between stdio tool calls. Add module-level fallback so login credentials persist across calls while still supporting per-session isolation for SSE transport. * feat: improve MCP server UX for agents - describe_component_type separates advanced fields from core ones - search_component_types accepts output_type filter - list_flows accepts query filter and includes ASCII graph repr - get_flow_info includes ASCII graph repr - add duplicate_flow tool - add list_starter_projects tool * feat: add use_starter_project tool and tests for new features - use_starter_project creates a flow from a starter template by name (starter projects aren't fetchable by ID via /flows/) - Tests for duplicate_flow, starter projects, graph repr, advanced fields, and output_type search * docs: improve MCP tool descriptions for agent clarity - Add server-level instructions with typical workflow guide - Remove internal implementation details from tool descriptions - Add cross-references between related tools - Mention component_as_tool and graph diagrams where relevant * docs: address subagent review feedback on tool descriptions - Document return values for create_flow, add_component - Clarify empty-query behavior for search_component_types - Distinguish get_component_info (instance) vs describe_component_type (type) - Explain connection type compatibility in instructions - Clarify configure_component trigger field behavior - State disconnect_components default when filters omitted * feat: add batch tool for multi-action requests Execute multiple actions in one call with $N.field references to chain results. An agent can build a complete flow in a single request instead of 6+ round trips. * feat: add create_flow_from_spec tool for compact text-based flow creation Accepts a compact text spec with nodes, edges (using real port names), and config sections. Agents generate a simple string instead of constructing nested JSON. Tool mode auto-enabled for component_as_tool. Handles Prompt Template dynamic variables by parsing {var} from template text and creating input fields. Cleans up flows on failure. Type coercion for numeric/boolean config values. * feat: add build_flow validation and create_flow_from_spec build_flow validates flows by building the graph server-side. create_flow_from_spec accepts a compact text spec with nodes, edges, and config. Validates by default (optional). Handles Prompt Template dynamic {variables}, auto-enables tool_mode for component_as_tool, cleans up on failure, coerces config types. * fix: address PR review feedback - Fix test fixture to use contextvars instead of stale module attributes - Raise ValueError on malformed spec lines instead of silently dropping - Disambiguate duplicate component types in flow_graph_repr - Narrow except Exception to ImportError in flow_graph_repr - Add action-index context to batch error messages - Fix stale/inaccurate docstrings (group count, "| ", field_name, category, build_flow) - Mention create_flow_from_spec in MCP instructions * feat: stream run_flow events via MCP progress notifications run_flow now consumes Langflow's SSE stream and relays token events to the MCP client via report_progress. Falls back to a regular POST if the stream yields no result. * test: add streaming integration tests for run_flow and stream_post * chore: rebuild component index * [autofix.ci] apply automated fixes * fix: handle Message dicts in str field param processing, add MCP logger param_handler's str case called unescape_string on list elements without type checking. On subsequent agent calls, chat history stores Message dicts in the list, causing 'dict' object has no attribute 'replace'. Added _coerce_str_value that extracts .text from Message/Data/dict objects. Added lfx logger to MCP server with streaming fallback warning. * feat: add flow builder tools, propose_field_edit, and flow_to_spec_summary - builder.py: builds flow dicts from text specs using local component registry with granular error handling per build phase - flow_builder_tools.py: 9 Langflow components for agent tooling (search, describe, get_field_value, propose_field_edit, add_component, remove_component, connect_components, configure_component, build_flow) - propose_field_edit generates validated JSON Patches with dry-run - flow_to_spec_summary converts flow dicts to compact summaries with IDs - Module-level event queue for real-time UI updates during streaming * [autofix.ci] apply automated fixes * feat: add get_build_results and get_component_output MCP tools Exposes per-component build data from the vertex_builds table: - get_build_results: returns all component outputs, validity, and errors from the last run -- useful for debugging which component failed - get_component_output: inspect a specific component's output from the last run to trace where the pipeline broke * feat: add flow management, iteration, and discovery MCP tools Response improvements: - spec_summary (component IDs + connection ports) in get_flow_info/list_flows - Merged components() tool: search or describe in one call Flow management tools: - validate_flow: polls build results with timeout, structured per-component errors - rename_flow: update name/description - export_flow: serialize to JSON with sensitive field redaction - update_flow_from_spec: declarative update with reference validation Component iteration tools: - freeze_component / unfreeze_component: skip re-execution during iteration - layout_flow_tool: re-layout after modifications Security: export_flow redacts API keys via redact_node before exposing to LLM. Includes 18 integration tests covering all new tools. * refactor: extract shared _node_id and validate_spec_references - _utils.py: shared node_id helper (was duplicated in component.py and layout.py) - spec.py: validate_spec_references extracted from three copies in create_flow_from_spec, update_flow_from_spec, and build_flow_from_spec * fix: update test fixture to use contextvars-based server API The mcp_client fixture was accessing mcp_server_module._client and ._registry directly, but these were replaced with contextvars (_client_var, _shared_client, _set_client, etc.) in the server module refactor. * [autofix.ci] apply automated fixes * fix: address review feedback on MCP server PR - Move flow_builder_tools out of components/ into mcp/ (fixes test_get_all) - Extract _set_frozen() helper to deduplicate freeze/unfreeze - Add missing tools to batch _TOOL_MAP - Fix sensitive field detection to use word-boundary matching - Unify redaction logic via shared is_sensitive_field() - Log skipped non-JSON SSE lines in stream_post - Rebuild component index * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * fix: gracefully handle server refresh failure in configure_component When a real_time_refresh field (e.g. model_name) is configured before its dependency (e.g. api_key), the server-side refresh fails. Instead of propagating a raw RuntimeError, the value is saved locally and a warning is returned telling the agent to set the credential first. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Keval718 <kevalvirat@gmail.com> Co-authored-by: Eric Hare <ericrhare@gmail.com>
1 parent c08a465 commit a2d3c18

19 files changed

Lines changed: 3628 additions & 152 deletions

src/backend/tests/unit/api/v1/test_mcp_client_server.py

Lines changed: 599 additions & 17 deletions
Large diffs are not rendered by default.

src/lfx/src/lfx/graph/flow_builder/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,24 @@
1616
list_connections,
1717
remove_connection,
1818
)
19-
from lfx.graph.flow_builder.flow import empty_flow, flow_info
19+
from lfx.graph.flow_builder.flow import empty_flow, flow_graph_repr, flow_info, flow_to_spec_summary
2020
from lfx.graph.flow_builder.layout import layout_flow
21+
from lfx.graph.flow_builder.spec import parse_flow_spec
2122

2223
__all__ = [
2324
"add_component",
2425
"add_connection",
2526
"configure_component",
2627
"empty_flow",
28+
"flow_graph_repr",
2729
"flow_info",
30+
"flow_to_spec_summary",
2831
"get_component",
2932
"layout_flow",
3033
"list_components",
3134
"list_connections",
3235
"needs_server_update",
36+
"parse_flow_spec",
3337
"remove_component",
3438
"remove_connection",
3539
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Shared utilities for flow_builder modules.
2+
3+
Extracted here to avoid duplicating helpers across component.py, layout.py,
4+
and other flow_builder modules that need the same node-level operations.
5+
"""
6+
7+
8+
def node_id(node: dict) -> str:
9+
"""Extract the node ID from a node dict.
10+
11+
Nodes store their ID in node["data"]["id"], falling back to node["id"].
12+
This was duplicated in component.py and layout.py -- consolidated here
13+
so both import from one place.
14+
"""
15+
return node.get("data", {}).get("id", node.get("id", ""))
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Build a complete flow from a text spec using the local component registry.
2+
3+
The component registry is loaded from a bundled index file and cached
4+
at module level. No network access or running server required.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import json
10+
from pathlib import Path
11+
from typing import Any
12+
13+
from lfx.graph.flow_builder.component import add_component, configure_component
14+
from lfx.graph.flow_builder.connect import add_connection
15+
from lfx.graph.flow_builder.flow import empty_flow
16+
from lfx.graph.flow_builder.layout import layout_flow
17+
from lfx.graph.flow_builder.spec import parse_flow_spec, validate_spec_references
18+
from lfx.log.logger import logger
19+
20+
_INDEX_PATH = Path(__file__).resolve().parent.parent.parent / "_assets" / "component_index.json"
21+
_registry_cache: dict[str, dict] | None = None
22+
23+
24+
def load_local_registry() -> dict[str, dict]:
25+
"""Load the component registry from the bundled index file.
26+
27+
Returns a flat dict: {component_type: template_dict}.
28+
Results are cached after the first call.
29+
30+
Raises:
31+
RuntimeError: If the index file is missing, corrupt, or empty.
32+
"""
33+
global _registry_cache # noqa: PLW0603
34+
if _registry_cache is not None:
35+
return _registry_cache
36+
37+
try:
38+
with _INDEX_PATH.open() as f:
39+
data = json.load(f)
40+
except FileNotFoundError:
41+
msg = f"Component registry not found at {_INDEX_PATH}. The lfx package may be installed incorrectly."
42+
raise RuntimeError(msg) from None
43+
except (json.JSONDecodeError, OSError) as e:
44+
msg = f"Failed to load component registry from {_INDEX_PATH}: {e}"
45+
raise RuntimeError(msg) from e
46+
47+
registry: dict[str, dict] = {}
48+
for cat in data.get("entries", []):
49+
if isinstance(cat, list) and len(cat) > 1 and isinstance(cat[1], dict):
50+
category_name = cat[0] if isinstance(cat[0], str) else ""
51+
for name, comp_data in cat[1].items():
52+
if isinstance(comp_data, dict) and "template" in comp_data:
53+
registry[name] = {**comp_data, "category": category_name}
54+
55+
if not registry:
56+
msg = f"Component registry at {_INDEX_PATH} contains no valid components."
57+
raise RuntimeError(msg)
58+
59+
logger.debug("Loaded %d components from local registry", len(registry))
60+
_registry_cache = registry
61+
return registry
62+
63+
64+
def build_flow_from_spec(spec: str) -> dict[str, Any]:
65+
"""Build a flow dict from a text spec. Returns the flow or errors.
66+
67+
On success: {"flow": <flow_dict>, "name": str, "node_count": int, "edge_count": int}
68+
On failure: {"error": str, "details": str}
69+
"""
70+
registry = load_local_registry()
71+
72+
try:
73+
parsed = parse_flow_spec(spec)
74+
except ValueError as e:
75+
return {"error": "Invalid spec", "details": str(e)}
76+
77+
# Validate that all component types exist in the registry
78+
unknown = [n["type"] for n in parsed["nodes"] if n["type"] not in registry]
79+
if unknown:
80+
return {
81+
"error": f"Unknown component types: {unknown}",
82+
"details": f"Available types (sample): {sorted(registry.keys())[:30]}",
83+
}
84+
85+
# Validate node references in edges and config
86+
try:
87+
validate_spec_references(parsed)
88+
except ValueError as e:
89+
return {"error": str(e), "details": str(e)}
90+
91+
# Build the flow
92+
flow = empty_flow(
93+
name=parsed.get("name", "Untitled Flow"),
94+
description=parsed.get("description", ""),
95+
)
96+
97+
id_map: dict[str, str] = {}
98+
99+
# Add components
100+
for node in parsed["nodes"]:
101+
try:
102+
result = add_component(flow, node["type"], registry)
103+
except (ValueError, KeyError) as e:
104+
return {"error": f"Failed to add component '{node['type']}' (node '{node['id']}')", "details": str(e)}
105+
id_map[node["id"]] = result["id"]
106+
107+
# Apply config
108+
for spec_id, params in parsed.get("config", {}).items():
109+
try:
110+
configure_component(flow, id_map[spec_id], params)
111+
except (ValueError, KeyError) as e:
112+
return {"error": f"Failed to configure node '{spec_id}'", "details": str(e)}
113+
114+
# Connect edges
115+
for edge in parsed["edges"]:
116+
src_out = f"{edge['source_id']}.{edge['source_output']}"
117+
tgt_in = f"{edge['target_id']}.{edge['target_input']}"
118+
try:
119+
add_connection(
120+
flow,
121+
id_map[edge["source_id"]],
122+
edge["source_output"],
123+
id_map[edge["target_id"]],
124+
edge["target_input"],
125+
)
126+
except (ValueError, KeyError) as e:
127+
return {"error": f"Failed to connect {src_out} -> {tgt_in}", "details": str(e)}
128+
129+
layout_flow(flow)
130+
131+
flow["name"] = parsed.get("name", "Untitled Flow")
132+
flow["description"] = parsed.get("description", "")
133+
134+
return {
135+
"flow": flow,
136+
"name": flow["name"],
137+
"node_count": len(flow["data"]["nodes"]),
138+
"edge_count": len(flow["data"]["edges"]),
139+
"node_id_map": id_map,
140+
}

src/lfx/src/lfx/graph/flow_builder/component.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import string
1515
from typing import Any
1616

17+
from lfx.graph.flow_builder._utils import node_id as _node_id
18+
1719

1820
def _generate_id(component_type: str) -> str:
1921
"""Generate a component ID like 'ChatInput-a1B2c'."""
@@ -162,11 +164,6 @@ def needs_server_update(template: dict, field: str) -> bool:
162164
return bool(field_def.get("real_time_refresh"))
163165

164166

165-
def _node_id(node: dict) -> str:
166-
"""Extract the node ID from a node dict."""
167-
return node.get("data", {}).get("id", node.get("id", ""))
168-
169-
170167
def _find_node(flow: dict, component_id: str) -> dict | None:
171168
"""Find a node by component ID."""
172169
for node in flow.get("data", {}).get("nodes", []):

src/lfx/src/lfx/graph/flow_builder/flow.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,89 @@ def flow_info(flow: dict) -> dict:
5757
"inputs": inputs,
5858
"outputs": outputs,
5959
}
60+
61+
62+
def flow_to_spec_summary(flow: dict) -> str:
63+
"""Convert a flow dict to a compact summary with component IDs for LLM context.
64+
65+
Includes full component IDs so the agent can reference them in tool calls.
66+
Field values are NOT included -- the agent should use get_field_value to inspect.
67+
"""
68+
data = flow.get("data", {})
69+
nodes = data.get("nodes", [])
70+
edges = data.get("edges", [])
71+
72+
if not nodes:
73+
return "(empty canvas)"
74+
75+
id_to_type: dict[str, str] = {}
76+
lines = [f"name: {flow.get('name', 'Untitled')}"]
77+
78+
lines.append("\ncomponents:")
79+
for node in nodes:
80+
nd = node.get("data", {})
81+
nid = nd.get("id", node.get("id", ""))
82+
ntype = nd.get("type", "?")
83+
id_to_type[nid] = ntype
84+
lines.append(f" {nid}: {ntype}")
85+
86+
if edges:
87+
lines.append("\nconnections:")
88+
for edge in edges:
89+
src = edge.get("source", "")
90+
tgt = edge.get("target", "")
91+
edge_data = edge.get("data", {})
92+
src_handle = edge_data.get("sourceHandle", {}).get("name", "?")
93+
tgt_handle = edge_data.get("targetHandle", {}).get("fieldName", "?")
94+
lines.append(f" {src}.{src_handle} -> {tgt}.{tgt_handle}")
95+
96+
return "\n".join(lines)
97+
98+
99+
def flow_graph_repr(flow: dict) -> str:
100+
"""Build an ASCII DAG representation of a flow's graph.
101+
102+
Uses lfx's ASCII graph renderer (grandalf-based Sugiyama layout),
103+
falling back to a simple chain representation if unavailable.
104+
"""
105+
data = flow.get("data", {})
106+
nodes = data.get("nodes", [])
107+
edges = data.get("edges", [])
108+
109+
if not nodes:
110+
return "(empty)"
111+
112+
# Build id -> label map, disambiguating duplicate types
113+
id_to_label: dict[str, str] = {}
114+
type_count: dict[str, int] = {}
115+
for node in nodes:
116+
nd = node.get("data", {})
117+
nid = nd.get("id", node.get("id", ""))
118+
node_type = nd.get("type", "?")
119+
count = type_count.get(node_type, 0) + 1
120+
type_count[node_type] = count
121+
id_to_label[nid] = f"{node_type} #{count}" if count > 1 else node_type
122+
123+
# Go back and suffix the first occurrence too when there are duplicates
124+
for nid, label in id_to_label.items():
125+
if "#" not in label and type_count.get(label, 0) > 1:
126+
id_to_label[nid] = f"{label} #1"
127+
128+
if not edges:
129+
return ", ".join(sorted(id_to_label.values()))
130+
131+
vertexes = list(id_to_label.values())
132+
edge_pairs = []
133+
for edge in edges:
134+
src_label = id_to_label.get(edge.get("source", ""))
135+
tgt_label = id_to_label.get(edge.get("target", ""))
136+
if src_label and tgt_label:
137+
edge_pairs.append((src_label, tgt_label))
138+
139+
try:
140+
from lfx.graph.graph.ascii import draw_graph
141+
142+
return draw_graph(vertexes, edge_pairs, return_ascii=True) or "(empty)"
143+
except ImportError:
144+
# grandalf not available; fall back to simple representation
145+
return ", ".join(f"{s} -> {t}" for s, t in edge_pairs)

src/lfx/src/lfx/graph/flow_builder/layout.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from collections import defaultdict, deque
1010

11+
from lfx.graph.flow_builder._utils import node_id as _node_id
12+
1113
LAYER_SPACING_X = 600
1214
NODE_SPACING_Y = 350
1315

@@ -86,7 +88,3 @@ def _assign_layers(
8688
next_layer += 1
8789

8890
return layers
89-
90-
91-
def _node_id(node: dict) -> str:
92-
return node.get("data", {}).get("id", node.get("id", ""))

0 commit comments

Comments
 (0)