fix(mcp): Wire credentials into MCP block form and add auto-lookup fallback

Frontend: Include credentials field in MCP block's dynamic input schema
so users can select OAuth credentials from the node form. Separate
credentials from tool_arguments in FormCreator to store them at the
correct level in hardcodedValues.

Backend: Add _auto_lookup_credential fallback in MCPToolBlock.run() for
legacy nodes that don't have credentials explicitly set. This resolves
the credential by matching mcp_server_url in stored OAuth metadata.
This commit is contained in:
Zamil Majdy
2026-02-10 14:05:09 +04:00
parent c03fb170e0
commit ed50f7f87d
3 changed files with 72 additions and 7 deletions

View File

@@ -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
)

View File

@@ -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<string, any>,
blockInputSchema: Record<string, any>,
): Record<string, any> {
// 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 ?? {})

View File

@@ -44,10 +44,13 @@ export const FormCreator: React.FC<FormCreatorProps> = 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<FormCreatorProps> = 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;
}