diff --git a/autogpt_platform/backend/backend/blocks/mcp/block.py b/autogpt_platform/backend/backend/blocks/mcp/block.py index f147ffdf5c..339f564f10 100644 --- a/autogpt_platform/backend/backend/blocks/mcp/block.py +++ b/autogpt_platform/backend/backend/blocks/mcp/block.py @@ -206,6 +206,45 @@ class MCPToolBlock(Block): return output_parts[0] return output_parts if output_parts else None + @staticmethod + async def _auto_lookup_credential( + user_id: str, server_url: str + ) -> "OAuth2Credentials | None": + """Auto-lookup stored MCP credential for a server URL. + + This is a fallback for nodes that don't have ``credentials`` explicitly + set (e.g. nodes created before the credential field was wired up). + """ + from backend.data.model import Credentials + from backend.integrations.creds_manager import IntegrationCredentialsManager + from backend.integrations.providers import ProviderName + + try: + mgr = IntegrationCredentialsManager() + mcp_creds: list[Credentials] = [] + for prov in (ProviderName.MCP.value, "ProviderName.MCP"): + mcp_creds.extend(await mgr.store.get_creds_by_provider(user_id, prov)) + best: OAuth2Credentials | None = None + for cred in mcp_creds: + if ( + isinstance(cred, OAuth2Credentials) + and cred.metadata.get("mcp_server_url") == server_url + ): + if best is None or ( + (cred.access_token_expires_at or 0) + > (best.access_token_expires_at or 0) + ): + best = cred + if best: + best = await mgr.refresh_if_needed(user_id, best) + logger.info( + "Auto-resolved MCP credential %s for %s", best.id, server_url + ) + return best + except Exception: + logger.debug("Auto-lookup MCP credential failed", exc_info=True) + return None + async def run( self, input_data: Input, @@ -222,6 +261,14 @@ class MCPToolBlock(Block): yield "error", "No tool selected. Please select a tool from the dropdown." return + # If no credentials were injected by the executor (e.g. legacy nodes + # that don't have the credentials field set), try to auto-lookup + # the stored MCP credential for this server URL. + if credentials is None: + credentials = await self._auto_lookup_credential( + user_id, input_data.server_url + ) + auth_token = ( credentials.access_token.get_secret_value() if credentials else None ) diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/useCustomNode.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/useCustomNode.tsx index 6445a339fe..52b491d68e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/useCustomNode.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/useCustomNode.tsx @@ -9,19 +9,25 @@ import { SpecialBlockID } from "@/lib/autogpt-server-api"; * Build a dynamic input schema for MCP blocks. * * When a tool has been selected (tool_input_schema is populated), the block - * renders only the selected tool's input parameters. Credentials are NOT - * included because authentication is already handled by the MCP dialog's - * OAuth flow and stored server-side. + * renders the selected tool's input parameters *plus* the credentials field + * so users can select/change the OAuth credential used for execution. * * Static fields like server_url, selected_tool, available_tools, and * tool_arguments are hidden because they're pre-configured from the dialog. */ function buildMCPInputSchema( toolInputSchema: Record, + blockInputSchema: Record, ): Record { + // Extract the credentials field from the block's original input schema + const credentialsSchema = + blockInputSchema?.properties?.credentials ?? undefined; + return { type: "object", properties: { + // Credentials field first so the dropdown appears at the top + ...(credentialsSchema ? { credentials: credentialsSchema } : {}), ...(toolInputSchema.properties ?? {}), }, required: [...(toolInputSchema.required ?? [])], @@ -50,7 +56,10 @@ export const useCustomNode = ({ const currentInputSchema = isAgent ? (data.hardcodedValues.input_schema ?? {}) : isMCPWithTool - ? buildMCPInputSchema(data.hardcodedValues.tool_input_schema) + ? buildMCPInputSchema( + data.hardcodedValues.tool_input_schema, + data.inputSchema, + ) : data.inputSchema; const currentOutputSchema = isAgent ? (data.hardcodedValues.output_schema ?? {}) diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/FormCreator.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/FormCreator.tsx index 839552c867..77b21dda92 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/FormCreator.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/FormCreator.tsx @@ -44,10 +44,13 @@ export const FormCreator: React.FC = React.memo( inputs: formData, }; } else if (isMCPWithTool) { - // All form fields are tool arguments (credentials handled by dialog) + // Separate credentials from tool arguments — credentials are stored + // at the top level of hardcodedValues, not inside tool_arguments. + const { credentials, ...toolArgs } = formData; updatedValues = { ...getHardCodedValues(nodeId), - tool_arguments: formData, + tool_arguments: toolArgs, + ...(credentials?.id ? { credentials } : {}), }; } else { updatedValues = formData; @@ -62,7 +65,13 @@ export const FormCreator: React.FC = React.memo( if (isAgent) { initialValues = hardcodedValues.inputs ?? {}; } else if (isMCPWithTool) { - initialValues = hardcodedValues.tool_arguments ?? {}; + // Merge tool arguments with credentials for the form + initialValues = { + ...(hardcodedValues.tool_arguments ?? {}), + ...(hardcodedValues.credentials?.id + ? { credentials: hardcodedValues.credentials } + : {}), + }; } else { initialValues = hardcodedValues; }