OpenBB-finance/OpenBB · error · TypeError

mcp_config must be a dictionary.

Error message

mcp_config must be a dictionary.

What it means

TypeError raised while extracting per-route MCP configuration: the route's openapi_extra['mcp_config'] (or 'x-mcp') value is present but is not a dictionary. In strict mode this raises; in non-strict mode the config is replaced with an empty dict (route simply gets no MCP annotations).

Source

Thrown at openbb_platform/extensions/mcp_server/openbb_mcp_server/utils/fastapi.py:72


def get_mcp_config(route: APIRoute, *, strict: bool = False) -> MCPConfigModel:
    """
    Read and validate per-route MCP config from openapi_extra.

    Args:
        route: The APIRoute to process.
        strict: If True, raise validation errors. If False, log warnings.

    Returns:
        A validated MCPConfigModel instance.
    """
    extra = route.openapi_extra or {}
    raw_config = extra.get("mcp_config") or extra.get("x-mcp") or {}

    if not isinstance(raw_config, dict):
        if strict:
            raise TypeError("mcp_config must be a dictionary.")
        raw_config = {}

    try:
        return validate_mcp_config(raw_config, strict=strict)
    except (ValidationError, TypeError, ValueError) as e:
        if strict:
            raise e from e
        return MCPConfigModel()


def _get_prompt_configs(route: APIRoute) -> list[dict]:
    """Extract prompt configurations from per-route MCP config.

    Supports a 'prompts' list of dicts.
    Returns a list of prompt configurations.
    """
    mcp_cfg = get_mcp_config(route)
    # Convert PromptConfigModel to dict

View on GitHub (pinned to 3e071fcc2c)

Solutions

  1. Make the value a dict: openapi_extra={"mcp_config": {"expose": True, "tags": ["news"]}}
  2. If a boolean 'expose this route' is all you need, put it inside the dict: {"mcp_config": {"expose": true}}
  3. For third-party routes you cannot edit, run the collector in non-strict mode so bad extras degrade to a warning

Example fix

# before
@app.get("/news", openapi_extra={"x-mcp": "true"})

# after
@app.get("/news", openapi_extra={"x-mcp": {"expose": True}})
Defensive patterns

Strategy: type-guard

Validate before calling

extra = route.openapi_extra or {}
raw = extra.get("mcp_config") or extra.get("x-mcp") or {}
if not isinstance(raw, dict):
    route.openapi_extra["mcp_config"] = {}  # or repair/log
    raw = {}
validated = validate_mcp_config(raw, strict=False)

Type guard

def route_has_dict_mcp_config(route) -> bool:
    extra = route.openapi_extra or {}
    raw = extra.get("mcp_config", extra.get("x-mcp", {}))
    return isinstance(raw, dict)

Try / catch

try:
    cfg = _get_route_mcp_config(route, strict=True)
except TypeError as e:
    if "must be a dictionary" in str(e):
        cfg = _get_route_mcp_config(route, strict=False)  # degrade to warning
    else:
        raise

Prevention

When it happens

Trigger: openapi_extra={"mcp_config": ["tags"]} or {"x-mcp": "true"} on a FastAPI route. Anything truthy that is not a dict fails the isinstance check — lists, strings, numbers, tuples.

Common situations: Confusing the schema-extension convention where x-mcp is sometimes written as a string flag, config generators emitting a JSON list, hand-written openapi_extra where braces were replaced by brackets, migrating from a version that tolerated scalar truthy values.

Related errors


AI-assisted analysis of OpenBB-finance/OpenBB@3e071fcc2c (2026-08-14). Data as JSON: /api/errors/f17457a62451081a. Report an issue: GitHub.