mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-10 06:45:28 -05:00
fix(mcp): Fix CI failures and credential validation issues
- Fix pyright errors in graph_test.py by properly typing frozenset[CredentialsType]
- Fix executor validation crash when credentials is empty {} by nullifying
the field before JSON schema validation
- Exclude MCP Tool block from e2e block discovery test (requires dialog)
- Normalize provider string in CredentialsMetaResponse to handle Python 3.13
str(Enum) bug for stored credentials
- Fix get_host() to match MCP provider regardless of enum string format
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -520,6 +520,9 @@ export class BuildPage extends BasePage {
|
||||
async getBlocksToSkip(): Promise<string[]> {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user