{"record":{"id":"3ffe53274a0b8432","repo":"BerriAI/litellm","slug":"forbidden","errorCode":"forbidden","errorMessage":"{tool_name} requires mcp_tool_search_enabled on the key","messagePattern":"(.+?) requires mcp_tool_search_enabled on the key","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"litellm/proxy/_experimental/mcp_server/rest_endpoints.py","lineNumber":179,"sourceCode":"    ) -> Any:\n        \"\"\"Handle the virtual ``mcp_tool_search`` / ``mcp_tool_call`` REST tools (gated on\n        ``mcp_tool_search_enabled``). Kept out of ``call_tool_rest_api`` so that endpoint stays a single\n        dispatch. An upstream 401 raised by the virtual ``mcp_tool_call`` propagates unhandled to the\n        caller's ``except MCPUpstreamAuthError`` relay, the same as the direct call path.\"\"\"\n        from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import (\n            MCPRequestHandler,\n        )\n        from litellm.proxy._experimental.mcp_server.tool_search import (\n            MCP_TOOL_SEARCH_TOOL_NAME,\n            coerce_top_k,\n            handle_mcp_tool_call,\n            handle_mcp_tool_search,\n        )\n        from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing\n        from litellm.proxy.proxy_server import general_settings, proxy_config, proxy_logging_obj\n\n        if not getattr(getattr(user_api_key_dict, \"object_permission\", None), \"mcp_tool_search_enabled\", False):\n            raise HTTPException(\n                status_code=403,\n                detail={\"error\": \"forbidden\", \"message\": f\"{tool_name} requires mcp_tool_search_enabled on the key\"},\n            )\n        tool_arguments: Final = data.get(\"arguments\") or {}\n        rest_client_ip: Final = IPAddressUtils.get_mcp_client_ip(request)\n        (\n            virtual_mcp_auth_header,\n            virtual_mcp_server_auth_headers,\n            virtual_raw_headers,\n        ) = _extract_mcp_headers_from_request(request, MCPRequestHandler)\n        virtual_oauth2_headers: Final = MCPRequestHandler._get_oauth2_headers_from_headers(request.headers)\n        if tool_name == MCP_TOOL_SEARCH_TOOL_NAME:\n            return await handle_mcp_tool_search(\n                query=tool_arguments.get(\"query\", \"\"),\n                top_k=coerce_top_k(tool_arguments.get(\"top_k\", 5)),\n                user_api_key_dict=user_api_key_dict,\n                client_ip=rest_client_ip,\n                mcp_auth_header=virtual_mcp_auth_header,","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/_experimental/mcp_server/rest_endpoints.py#L161-L197","documentation":"The virtual tools mcp_tool_search and mcp_tool_call are opt-in per API key: the REST facade checks object_permission.mcp_tool_search_enabled before handling them and returns 403 forbidden with this message when the flag is absent. Concrete per-server tool calls are unaffected.","triggerScenarios":"POST to the MCP tool-call REST route with tool_name mcp_tool_search or mcp_tool_call using a key whose object_permissions do not include mcp_tool_search_enabled=true (the attribute may also be entirely absent on older keys).","commonSituations":"Teams that enabled tool search only on specific keys; keys created before the feature existed; dashboard-generated keys where the tool-search checkbox was never ticked; moving a workload from a trial key to a production key without copying permissions.","solutions":["Enable the flag on the key: POST /key/update with object_permission.mcp_tool_search_enabled=true, or toggle it in the dashboard key editor.","Or grant it broadly via the team's object permissions so every team key inherits it.","Or skip the virtual tools and call the concrete tool directly with server_id + tool_name, which requires no flag."],"exampleFix":"# before\ncurl -X POST $PROXY/mcp/tool-call -H \"Authorization: Bearer $KEY\" \\\n  -d '{\"tool_name\": \"mcp_tool_search\", \"arguments\": {\"query\": \"deploy\"}}'\n# -> 403 {\"error\":\"forbidden\",\"message\":\"mcp_tool_search requires mcp_tool_search_enabled on the key\"}\n\n# after\ncurl -X POST $PROXY/key/update -H \"Authorization: Bearer $ADMIN_KEY\" \\\n  -d '{\"key\": \"sk-...\", \"object_permission\": {\"mcp_tool_search_enabled\": true}}'","handlingStrategy":"validation","validationCode":"VIRTUAL_TOOLS = {\"mcp_tool_search\", \"mcp_tool_call\"}\n\nasync def key_allows_tool_search(key_info: dict) -> bool:\n    perm = key_info.get(\"object_permission\") or key_info.get(\"object_permissions\") or {}\n    return bool(perm.get(\"mcp_tool_search_enabled\"))\n\nif tool_name in VIRTUAL_TOOLS and not await key_allows_tool_search(await get_key_info()):\n    raise PermissionError(\"enable mcp_tool_search_enabled on the key first\")","typeGuard":null,"tryCatchPattern":"resp = await client.post(f\"{proxy}/mcp/tool-call\", json=payload, headers=headers)\nif resp.status_code == 403 and resp.json().get(\"detail\", {}).get(\"error\") == \"forbidden\":\n    if \"mcp_tool_search_enabled\" in resp.text:\n        raise PermissionError(\"tool search not enabled for this key - update the key or call the concrete tool\") from None\nresp.raise_for_status()","preventionTips":["Encode the flag requirement in provisioning: any key intended for tool search gets mcp_tool_search_enabled at creation.","Fail fast in your client by checking key metadata before issuing virtual-tool calls.","Prefer concrete server_id + tool_name calls when search is not needed - no flag required."],"tags":["mcp","authorization","http-403","tool-search","api-key","permissions"],"backgroundTag":"api-key-permission-denied","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-24T22:17:12.610Z"}