{"record":{"id":"ecfa59d56f72f836","repo":"BerriAI/litellm","slug":"mcp-tools-config-must-be-a-list-of-dictionaries","errorCode":null,"errorMessage":"mcp_tools_config must be a list of dictionaries","messagePattern":"mcp_tools_config must be a list of dictionaries","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/_experimental/mcp_server/tool_registry.py","lineNumber":108,"sourceCode":"        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)\n\n            if handler is None:\n                verbose_logger.warning(\"Warning: Could not find handler %s for tool %s\", handler_name, name)\n                continue","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/_experimental/mcp_server/tool_registry.py#L90-L126","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# config.yaml - before\nmcp_tools:\n  - tools.weather.get_weather\n\n# after\nmcp_tools:\n  - name: get_weather\n    description: Get weather\n    handler: tools.weather.get_weather","handlingStrategy":"validation","validationCode":"def validate_mcp_tools_entries(cfg) -> list[str]:\n    if cfg is None:\n        return [\"mcp_tools missing\"]\n    bad = [i for i, entry in enumerate(cfg) if not isinstance(entry, dict)]\n    return [f\"entry {i} is not a dict (check YAML indentation)\" for i in bad]","typeGuard":"def is_valid_mcp_tools_config(cfg) -> bool:\n    return isinstance(cfg, list) and all(\n        isinstance(e, dict) and {\"name\", \"description\", \"handler\"} <= set(e) for e in cfg\n    )","tryCatchPattern":"try:\n    registry.load_tools_from_config(cfg[\"mcp_tools\"])\nexcept ValueError as e:\n    if \"list of dictionaries\" in str(e):\n        raise ConfigError(\"mcp_tools entries must be mappings: - name: ... description: ... handler: ...\") from e\n    raise","preventionTips":["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"],"tags":["mcp","config","yaml","value-error","litellm-proxy","validation"],"backgroundTag":"config-schema-validation","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}