From 4c02cd8f2f582f4afdbf9d4079117d1ce39a671b Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 10 Feb 2026 08:53:15 +0400 Subject: [PATCH] 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. --- .../backend/backend/executor/utils.py | 14 ++++++++++---- .../webhooks/graph_lifecycle_hooks.py | 15 +++++++++++++++ .../build/components/legacy-builder/Flow/Flow.tsx | 4 ++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/autogpt_platform/backend/backend/executor/utils.py b/autogpt_platform/backend/backend/executor/utils.py index 4461ea74b3..1face08600 100644 --- a/autogpt_platform/backend/backend/executor/utils.py +++ b/autogpt_platform/backend/backend/executor/utils.py @@ -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( diff --git a/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py b/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py index 5fb9198c4d..678d6ca12d 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py @@ -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']}" diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/Flow.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/Flow.tsx index 67b3cad9af..f566143f40 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/Flow.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/legacy-builder/Flow/Flow.tsx @@ -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 }, + }), }, };