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 dictView on GitHub (pinned to 3e071fcc2c)
Solutions
- Make the value a dict: openapi_extra={"mcp_config": {"expose": True, "tags": ["news"]}}
- If a boolean 'expose this route' is all you need, put it inside the dict: {"mcp_config": {"expose": true}}
- 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
- Always write mcp_config/x-mcp as an object, never a list or string flag
- Add a test that walks all routes and asserts isinstance(extra['mcp_config'], dict)
- Use strict=False when scanning third-party routers you do not control
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
- Tag cannot be empty string
- methods must be a list of strings
- Method '*' cannot be mixed with other HTTP methods.
- Invalid HTTP method '{method}'. Valid methods: {', '.join(va
- Category '{category}' not found. Available categories: {', '
AI-assisted analysis of OpenBB-finance/OpenBB@3e071fcc2c (2026-08-14).
Data as JSON: /api/errors/f17457a62451081a.
Report an issue: GitHub.