diff --git a/autogpt_platform/backend/backend/api/features/integrations/router.py b/autogpt_platform/backend/backend/api/features/integrations/router.py index 5e00924c39..f92795366e 100644 --- a/autogpt_platform/backend/backend/api/features/integrations/router.py +++ b/autogpt_platform/backend/backend/api/features/integrations/router.py @@ -1,7 +1,7 @@ import asyncio import logging from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Annotated, List, Literal +from typing import TYPE_CHECKING, Annotated, Any, List, Literal from autogpt_libs.auth import get_user_id from fastapi import ( @@ -14,7 +14,7 @@ from fastapi import ( Security, status, ) -from pydantic import BaseModel, Field, SecretStr +from pydantic import BaseModel, Field, SecretStr, model_validator from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR, HTTP_502_BAD_GATEWAY from backend.api.features.library.db import set_preset_webhook, update_preset @@ -106,12 +106,30 @@ class CredentialsMetaResponse(BaseModel): description="Host pattern for host-scoped or MCP server URL for MCP credentials", ) + @model_validator(mode="before") + @classmethod + def _normalize_provider(cls, data: Any) -> Any: + """Fix ``ProviderName.X`` format from Python 3.13 ``str(Enum)`` bug.""" + if isinstance(data, dict): + prov = data.get("provider", "") + if isinstance(prov, str) and prov.startswith("ProviderName."): + member = prov.removeprefix("ProviderName.") + try: + data = {**data, "provider": ProviderName[member].value} + except KeyError: + pass + return data + @staticmethod def get_host(cred: Credentials) -> str | None: """Extract host from credential: HostScoped host or MCP server URL.""" if isinstance(cred, HostScopedCredentials): return cred.host - if isinstance(cred, OAuth2Credentials) and cred.provider == ProviderName.MCP: + if isinstance(cred, OAuth2Credentials) and cred.provider in ( + ProviderName.MCP, + ProviderName.MCP.value, + "ProviderName.MCP", + ): return (cred.metadata or {}).get("mcp_server_url") return None diff --git a/autogpt_platform/backend/backend/data/graph_test.py b/autogpt_platform/backend/backend/data/graph_test.py index 7a545cf11e..5480e0eb79 100644 --- a/autogpt_platform/backend/backend/data/graph_test.py +++ b/autogpt_platform/backend/backend/data/graph_test.py @@ -473,19 +473,21 @@ def test_node_credentials_optional_with_other_metadata(): def test_mcp_credential_combine_different_servers(): """Two MCP credential fields with different server URLs should produce separate entries when combined (not merged into one).""" - from backend.data.model import CredentialsFieldInfo + from backend.data.model import CredentialsFieldInfo, CredentialsType from backend.integrations.providers import ProviderName + oauth2_types: frozenset[CredentialsType] = frozenset(["oauth2"]) + field_sentry = CredentialsFieldInfo( credentials_provider=frozenset([ProviderName.MCP]), - credentials_types=frozenset(["oauth2"]), + credentials_types=oauth2_types, credentials_scopes=None, discriminator="server_url", discriminator_values={"https://mcp.sentry.dev/mcp"}, ) field_linear = CredentialsFieldInfo( credentials_provider=frozenset([ProviderName.MCP]), - credentials_types=frozenset(["oauth2"]), + credentials_types=oauth2_types, credentials_scopes=None, discriminator="server_url", discriminator_values={"https://mcp.linear.app/mcp"}, @@ -515,19 +517,21 @@ def test_mcp_credential_combine_different_servers(): def test_mcp_credential_combine_same_server(): """Two MCP credential fields with the same server URL should be combined into one credential entry.""" - from backend.data.model import CredentialsFieldInfo + from backend.data.model import CredentialsFieldInfo, CredentialsType from backend.integrations.providers import ProviderName + oauth2_types: frozenset[CredentialsType] = frozenset(["oauth2"]) + field_a = CredentialsFieldInfo( credentials_provider=frozenset([ProviderName.MCP]), - credentials_types=frozenset(["oauth2"]), + credentials_types=oauth2_types, credentials_scopes=None, discriminator="server_url", discriminator_values={"https://mcp.sentry.dev/mcp"}, ) field_b = CredentialsFieldInfo( credentials_provider=frozenset([ProviderName.MCP]), - credentials_types=frozenset(["oauth2"]), + credentials_types=oauth2_types, credentials_scopes=None, discriminator="server_url", discriminator_values={"https://mcp.sentry.dev/mcp"}, @@ -548,18 +552,20 @@ def test_mcp_credential_combine_same_server(): def test_mcp_credential_combine_no_discriminator_values(): """MCP credential fields without discriminator_values should be merged into a single entry (backwards compat for blocks without server_url set).""" - from backend.data.model import CredentialsFieldInfo + from backend.data.model import CredentialsFieldInfo, CredentialsType from backend.integrations.providers import ProviderName + oauth2_types: frozenset[CredentialsType] = frozenset(["oauth2"]) + field_a = CredentialsFieldInfo( credentials_provider=frozenset([ProviderName.MCP]), - credentials_types=frozenset(["oauth2"]), + credentials_types=oauth2_types, credentials_scopes=None, discriminator="server_url", ) field_b = CredentialsFieldInfo( credentials_provider=frozenset([ProviderName.MCP]), - credentials_types=frozenset(["oauth2"]), + credentials_types=oauth2_types, credentials_scopes=None, discriminator="server_url", ) diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index 317be53d55..acd77ee2c3 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -274,7 +274,10 @@ async def execute_node( if not field_value or ( isinstance(field_value, dict) and not field_value.get("id") ): - continue # No credentials configured — block runs without + # No credentials configured — nullify so JSON schema validation + # doesn't choke on the empty default `{}`. + input_data[field_name] = None + continue # Block runs without credentials credentials_meta = input_type(**field_value) # Write normalized values back so JSON schema validation also passes diff --git a/autogpt_platform/frontend/src/tests/pages/build.page.ts b/autogpt_platform/frontend/src/tests/pages/build.page.ts index 8acc9a8f40..f48036a853 100644 --- a/autogpt_platform/frontend/src/tests/pages/build.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/build.page.ts @@ -520,6 +520,9 @@ export class BuildPage extends BasePage { async getBlocksToSkip(): Promise { return [ (await this.getGithubTriggerBlockDetails()).map((b) => b.id), + // MCP Tool block requires an interactive dialog (server URL + OAuth) before + // it can be placed, so it can't be tested via the standard "add block" flow. + "a0a4b1c2-d3e4-4f56-a7b8-c9d0e1f2a3b4", ].flat(); }