fix(mcp): Handle optional credentials in graph save and execution validation

- _on_graph_activate: Clear stale credential references for optional
  fields instead of blocking the save. Checks both node metadata
  (credentials_optional) and block schema (field not in required_fields).
- _validate_node_input_credentials: Use block schema's required_fields
  as fallback for credentials_optional check, so MCP blocks with
  default={} credentials are properly treated as optional.
- Set credentials_optional metadata on new MCP nodes in the frontend.
This commit is contained in:
Zamil Majdy
2026-02-10 08:53:15 +04:00
parent 909f313e1e
commit 4c02cd8f2f
3 changed files with 29 additions and 4 deletions

View File

@@ -265,7 +265,13 @@ async def _validate_node_input_credentials(
# Track if any credential field is missing for this node
has_missing_credentials = False
# A credential field is optional if the node metadata says so, or if
# the block schema declares a default for the field.
required_fields = block.input_schema.get_required_fields()
is_creds_optional = node.credentials_optional
for field_name, credentials_meta_type in credentials_fields.items():
field_is_optional = is_creds_optional or field_name not in required_fields
try:
# Check nodes_input_masks first, then input_default
field_value = None
@@ -278,7 +284,7 @@ async def _validate_node_input_credentials(
elif field_name in node.input_default:
# For optional credentials, don't use input_default - treat as missing
# This prevents stale credential IDs from failing validation
if node.credentials_optional:
if field_is_optional:
field_value = None
else:
field_value = node.input_default[field_name]
@@ -288,8 +294,8 @@ async def _validate_node_input_credentials(
isinstance(field_value, dict) and not field_value.get("id")
):
has_missing_credentials = True
# If node has credentials_optional flag, mark for skipping instead of error
if node.credentials_optional:
# If credential field is optional, skip instead of error
if field_is_optional:
continue # Don't add error, will be marked for skip after loop
else:
credential_errors[node.id][
@@ -343,7 +349,7 @@ async def _validate_node_input_credentials(
# The executor will pass credentials=None to the block's run().
if (
has_missing_credentials
and node.credentials_optional
and is_creds_optional
and node.id not in credential_errors
):
logger.info(

View File

@@ -50,6 +50,21 @@ async def _on_graph_activate(graph: "BaseGraph | GraphModel", user_id: str):
if (
creds_meta := new_node.input_default.get(creds_field_name)
) and not await get_credentials(creds_meta["id"]):
# If the credential field is optional (has a default in the
# schema, or node metadata marks it optional), clear the stale
# reference instead of blocking the save.
creds_field_optional = (
new_node.credentials_optional
or creds_field_name not in block_input_schema.get_required_fields()
)
if creds_field_optional:
new_node.input_default[creds_field_name] = {}
logger.warning(
f"Node #{new_node.id}: cleared stale optional "
f"credentials #{creds_meta['id']} for "
f"'{creds_field_name}'"
)
continue
raise ValueError(
f"Node #{new_node.id} input '{creds_field_name}' updated with "
f"non-existent credentials #{creds_meta['id']}"

View File

@@ -748,6 +748,10 @@ const FlowEditor: React.FC<{
block_id: blockID,
isOutputStatic: nodeSchema.staticOutput,
uiType: nodeSchema.uiType,
// MCP blocks have optional credentials (public servers don't need auth)
...(blockID === SpecialBlockID.MCP_TOOL && {
metadata: { credentials_optional: true },
}),
},
};