{"record":{"id":"22c98703aeea169d","repo":"BerriAI/litellm","slug":"mcp-tools-config-is-required-please-set-mcp-tool","errorCode":null,"errorMessage":"mcp_tools_config is required, please set `mcp_tools` in your proxy config","messagePattern":"mcp_tools_config is required, please set `mcp_tools` in your proxy config","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/_experimental/mcp_server/tool_registry.py","lineNumber":104,"sourceCode":"\n    def load_tools_from_config(\n        self,\n        mcp_tools_config: dict[str, Any] | None = None,\n        config_file_path: str | None = None,\n    ) -> None:\n        \"\"\"\n        Load and register tools from the proxy config\n\n        Args:\n            mcp_tools_config: The mcp_tools config from the proxy config\n            config_file_path: Path to the operator's config.yaml. Threaded\n                through to ``get_instance_fn`` so an ``s3://``/``gcs://``\n                ``handler`` declared in the YAML resolves; callers from a\n                non-YAML path must leave this ``None`` so the runtime gate\n                fires.\n        \"\"\"\n        if mcp_tools_config is None:\n            raise ValueError(\"mcp_tools_config is required, please set `mcp_tools` in your proxy config\")\n\n        for tool_config in mcp_tools_config:\n            if not isinstance(tool_config, dict):\n                raise ValueError(\"mcp_tools_config must be a list of dictionaries\")\n\n            name = tool_config.get(\"name\")\n            description = tool_config.get(\"description\")\n            input_schema = tool_config.get(\"input_schema\", {})\n            handler_name = tool_config.get(\"handler\")\n\n            if not all([name, description, handler_name]):\n                continue\n\n            # Try to resolve the handler\n            # First check if it's a module path (e.g., \"module.submodule.function\")\n            if handler_name is None:\n                raise ValueError(f\"handler is required for tool {name}\")\n            handler = get_instance_fn(handler_name, config_file_path)","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/_experimental/mcp_server/tool_registry.py#L86-L122","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# config.yaml - before: section missing -> load_tools_from_config(None)\n# after\ntop-level:\nmcp_tools:\n  - name: get_weather\n    description: Get weather\n    handler: tools.weather.get_weather","handlingStrategy":"validation","validationCode":"def get_mcp_tools_or_none(config: dict):\n    tools = config.get(\"mcp_tools\")\n    if tools is None:\n        return None  # caller skips loading; do not call load_tools_from_config(None)\n    return tools","typeGuard":"def has_mcp_tools_section(config: dict) -> bool:\n    return isinstance(config, dict) and isinstance(config.get(\"mcp_tools\"), list)","tryCatchPattern":"try:\n    registry.load_tools_from_config(cfg.get(\"mcp_tools\"))\nexcept ValueError as e:\n    if \"mcp_tools_config is required\" in str(e):\n        skip_local_tools()  # section intentionally absent\n    else:\n        raise","preventionTips":["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"],"tags":["mcp","config","value-error","litellm-proxy","startup"],"backgroundTag":"missing-config-value","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}