BerriAI/litellm · error · ValueError
mcp_tools_config must be a list of dictionaries
Error message
mcp_tools_config must be a list of dictionaries
What it means
ValueError from MCPToolRegistry.load_tools_from_config (tool_registry.py:108): iteration hit an element of mcp_tools that is not a dict. The loader requires every entry to be a mapping with name/description/handler keys; a YAML formatting mistake (list of strings, scalars, or a nested list) produces this immediately on the first bad element.
Source
Thrown at litellm/proxy/_experimental/mcp_server/tool_registry.py:108
config_file_path: str | None = None,
) -> None:
"""
Load and register tools from the proxy config
Args:
mcp_tools_config: The mcp_tools config from the proxy config
config_file_path: Path to the operator's config.yaml. Threaded
through to ``get_instance_fn`` so an ``s3://``/``gcs://``
``handler`` declared in the YAML resolves; callers from a
non-YAML path must leave this ``None`` so the runtime gate
fires.
"""
if mcp_tools_config is None:
raise ValueError("mcp_tools_config is required, please set `mcp_tools` in your proxy config")
for tool_config in mcp_tools_config:
if not isinstance(tool_config, dict):
raise ValueError("mcp_tools_config must be a list of dictionaries")
name = tool_config.get("name")
description = tool_config.get("description")
input_schema = tool_config.get("input_schema", {})
handler_name = tool_config.get("handler")
if not all([name, description, handler_name]):
continue
# Try to resolve the handler
# First check if it's a module path (e.g., "module.submodule.function")
if handler_name is None:
raise ValueError(f"handler is required for tool {name}")
handler = get_instance_fn(handler_name, config_file_path)
if handler is None:
verbose_logger.warning("Warning: Could not find handler %s for tool %s", handler_name, name)
continueView on GitHub (pinned to 77b7c6c40c)
Solutions
- Rewrite mcp_tools as a list of mappings, each with name, description, and handler keys, all indented under the dash
- Lint the config before startup: every entry must be a dict (see validation snippet)
- Check for tabs/spacing mistakes around the dash items in YAML
- If you want a path-shorthand, expand it to full dicts upstream of the loader
Example fix
# config.yaml - before
mcp_tools:
- tools.weather.get_weather
# after
mcp_tools:
- name: get_weather
description: Get weather
handler: tools.weather.get_weather Defensive patterns
Strategy: validation
Validate before calling
def validate_mcp_tools_entries(cfg) -> list[str]:
if cfg is None:
return ["mcp_tools missing"]
bad = [i for i, entry in enumerate(cfg) if not isinstance(entry, dict)]
return [f"entry {i} is not a dict (check YAML indentation)" for i in bad] Type guard
def is_valid_mcp_tools_config(cfg) -> bool:
return isinstance(cfg, list) and all(
isinstance(e, dict) and {"name", "description", "handler"} <= set(e) for e in cfg
) Try / catch
try:
registry.load_tools_from_config(cfg["mcp_tools"])
except ValueError as e:
if "list of dictionaries" in str(e):
raise ConfigError("mcp_tools entries must be mappings: - name: ... description: ... handler: ...") from e
raise Prevention
- Lint mcp_tools shape in CI (every entry a dict with name/description/handler)
- Prefer YAML anchors/examples from the repo docs when editing; watch dash-item indentation
- Validate before proxy startup so config typos fail fast in deploy, not at runtime
When it happens
Trigger: config.yaml mcp_tools written as a list of strings (e.g. - tools.weather.get_weather); a YAML dash item that parses as a scalar because keys were not indented under it; mixing a handler-name shorthand into the list; programmatic callers passing a list of module paths instead of dicts.
Common situations: Hand-editing YAML and losing one level of indentation so `- name: x` collapses to a string; converting from another tool format that lists handler paths only; copy-pasting examples that use a shorthand the loader never supported.
Related errors
- mcp_tools_config is required, please set `mcp_tools` in your
- Invalid budget_reset_time {raw!r}; must be a quoted 24-hour
- Custom code guardrail requires a guardrail_name
- Custom code guardrail requires 'custom_code' in litellm_para
- Invalid budget_reset_time {raw!r}; expected a 24-hour 'HH:MM
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/ecfa59d56f72f836.
Report an issue: GitHub.