BerriAI/litellm · error · ValueError
mcp_tools_config is required, please set `mcp_tools` in your
Error message
mcp_tools_config is required, please set `mcp_tools` in your proxy config
What it means
ValueError from MCPToolRegistry.load_tools_from_config (tool_registry.py:104): it was invoked with mcp_tools_config=None. The loader is meant to receive the mcp_tools list from the proxy config; None means the operator's config.yaml has no mcp_tools section (or the caller passed nothing), so the registry cannot be populated.
Source
Thrown at litellm/proxy/_experimental/mcp_server/tool_registry.py:104
def load_tools_from_config(
self,
mcp_tools_config: dict[str, Any] | None = None,
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)View on GitHub (pinned to 77b7c6c40c)
Solutions
- Add a top-level mcp_tools: section to config.yaml with at least one tool entry (list of dicts with name/description/handler)
- If MCP tools are intentionally unused, remove the custom code path that calls load_tools_from_config unconditionally
- Guard programmatic calls: skip loading when the config value is None instead of passing it through
- Validate config.yaml structure (mcp_tools at top level) before startup
Example fix
# config.yaml - before: section missing -> load_tools_from_config(None)
# after
top-level:
mcp_tools:
- name: get_weather
description: Get weather
handler: tools.weather.get_weather Defensive patterns
Strategy: validation
Validate before calling
def get_mcp_tools_or_none(config: dict):
tools = config.get("mcp_tools")
if tools is None:
return None # caller skips loading; do not call load_tools_from_config(None)
return tools Type guard
def has_mcp_tools_section(config: dict) -> bool:
return isinstance(config, dict) and isinstance(config.get("mcp_tools"), list) Try / catch
try:
registry.load_tools_from_config(cfg.get("mcp_tools"))
except ValueError as e:
if "mcp_tools_config is required" in str(e):
skip_local_tools() # section intentionally absent
else:
raise Prevention
- Check for the mcp_tools key before calling the loader; None is a config error, not an input
- Keep mcp_tools at the YAML top level and lint its presence/shape in CI
- Only invoke the loader from the config-loading path, never speculatively
When it happens
Trigger: Starting/proxy code path or custom script calls load_tools_from_config() with no argument while config.yaml lacks an mcp_tools key; config loading returned None because mcp_tools was misindented or placed under the wrong top-level key; programmatic use of MCPToolRegistry without a config dict.
Common situations: YAML indentation puts mcp_tools under litellm_settings or general_settings instead of the top level; ops removes the mcp_tools block but a custom startup hook still unconditionally loads tools; tests calling the loader directly with None.
Related errors
- mcp_tools_config must be a list of dictionaries
- MCPJWTSigner guardrail requires a guardrail_name
- MCPJWTSigner: ttl_seconds must be > 0, got {resolved_ttl}
- MCP Security: guardrail_name is required
- MCP SDK is not installed. Please install it with: pip instal
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/22c98703aeea169d.
Report an issue: GitHub.