From dd4249db7815e3ad83a5a7d0e5d5ae609c41c98d Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 16:27:01 -0700 Subject: [PATCH 1/6] feat(mcp_server_manager.py): initial commit adding allowed params support allow admin to specify which parameters to allow/disallow by MCP tool --- .../mcp_server/mcp_server_manager.py | 75 ++++++++++++++++++- litellm/proxy/_new_secret_config.yaml | 39 +++++++++- .../types/mcp_server/mcp_server_manager.py | 3 + 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 10e40b76efd..129a756e4f5 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -215,6 +215,7 @@ class MCPServerManager: extra_headers=server_config.get("extra_headers", None), allowed_tools=server_config.get("allowed_tools", None), disallowed_tools=server_config.get("disallowed_tools", None), + allowed_params=server_config.get("allowed_params", None), access_groups=server_config.get("access_groups", None), ) self.config_mcp_servers[server_id] = new_server @@ -602,6 +603,60 @@ class MCPServerManager: return tool_name not in server.disallowed_tools return True + def filter_allowed_params( + self, tool_name: str, arguments: Dict[str, Any], server: MCPServer + ) -> Dict[str, Any]: + """ + Filter arguments to only include allowed parameters for the given tool. + + Args: + tool_name: Name of the tool (with or without prefix) + arguments: Dictionary of arguments to filter + server: MCPServer configuration + + Returns: + Filtered dictionary containing only allowed parameters + + Raises: + HTTPException: If allowed_params is configured for this tool but arguments contain disallowed params + """ + from litellm.proxy._experimental.mcp_server.utils import ( + get_server_name_prefix_tool_mcp, + ) + + # If no allowed_params configured, return all arguments + if not server.allowed_params: + return arguments + + # Get the unprefixed tool name to match against config + unprefixed_tool_name, _ = get_server_name_prefix_tool_mcp(tool_name) + + # Check both prefixed and unprefixed tool names + allowed_params_list = server.allowed_params.get( + tool_name + ) or server.allowed_params.get(unprefixed_tool_name) + + # If this tool doesn't have allowed_params specified, allow all params + if allowed_params_list is None: + return arguments + + # Filter arguments to only include allowed parameters + disallowed_params = [ + param for param in arguments.keys() if param not in allowed_params_list + ] + + if disallowed_params: + raise HTTPException( + status_code=403, + detail={ + "error": f"Parameters {disallowed_params} are not allowed for tool {tool_name}. " + f"Allowed parameters: {allowed_params_list}. " + f"Contact proxy admin to allow these parameters." + }, + ) + + return {k: v for k, v in arguments.items() if k in allowed_params_list} + async def check_tool_permission_for_key_team( self, tool_name: str, @@ -621,18 +676,20 @@ class MCPServerManager: Raises: HTTPException: If tool is not allowed for this key/team """ - from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import MCPRequestHandler - + from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import ( + MCPRequestHandler, + ) + if not user_api_key_auth: return - + # Check if tool is allowed is_allowed = await MCPRequestHandler.is_tool_allowed_for_server( tool_name=tool_name, server_id=server.server_id, user_api_key_auth=user_api_key_auth, ) - + if not is_allowed: raise HTTPException( status_code=403, @@ -667,6 +724,16 @@ class MCPServerManager: user_api_key_auth=user_api_key_auth, ) + ## filter parameters based on allowed_params configuration + filtered_arguments = self.filter_allowed_params( + tool_name=name, + arguments=arguments, + server=server, + ) + # Update arguments with filtered version + arguments.clear() + arguments.update(filtered_arguments) + pre_hook_kwargs = { "name": name, "arguments": arguments, diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index 52a6fc16ff1..b2f4635ac99 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -1,6 +1,39 @@ model_list: - model_name: gpt-5-mini litellm_params: - model: azure/gpt-5-mini-2 - api_key: os.environ/AZURE_API_KEY_ALT - api_base: os.environ/AZURE_API_BASE_ALT + model: openai/gpt-4o-mini + api_base: "https://webhook.site/2f385e05-00aa-402b-86d1-efc9261471a5" + api_key: dummy + - model_name: "byok-wildcard/*" + litellm_params: + model: openai/* + - model_name: xai-grok-3 + litellm_params: + model: xai/grok-3 + - model_name: hosted_vllm/whisper-v3 + litellm_params: + model: hosted_vllm/whisper-v3 + api_base: "https://webhook.site/2f385e05-00aa-402b-86d1-efc9261471a5" + api_key: dummy + +mcp_servers: + my_api_mcp: + url: "http://0.0.0.0:8090" + spec_path: "/Users/krrishdholakia/Documents/temp_py_folder/example_openapi.json" + auth_type: none + allowed_tools: ["getpetbyid", "my_api_mcp-findpetsbystatus"] + # Configure allowed parameters per tool + # Key: tool name (with or without prefix) + # Value: list of allowed parameter names + allowed_params: + # Using unprefixed tool name + "getpetbyid": ["petId"] + # Using prefixed tool name (both formats work) + "my_api_mcp-findpetsbystatus": ["status", "limit"] + # Example: allow only specific params for another tool + # "another_tool": ["param1", "param2"] + + +litellm_settings: + callbacks: ["prometheus"] + custom_prometheus_metadata_labels: ["metadata.initiative", "metadata.business-unit"] diff --git a/litellm/types/mcp_server/mcp_server_manager.py b/litellm/types/mcp_server/mcp_server_manager.py index 3e0c2b20e39..81c116bdab8 100644 --- a/litellm/types/mcp_server/mcp_server_manager.py +++ b/litellm/types/mcp_server/mcp_server_manager.py @@ -25,6 +25,9 @@ class MCPServer(BaseModel): ) allowed_tools: Optional[List[str]] = None disallowed_tools: Optional[List[str]] = None + allowed_params: Optional[Dict[str, List[str]]] = ( + None # map of tool names to allowed parameter lists + ) # OAuth-specific fields client_id: Optional[str] = None client_secret: Optional[str] = None From ecb6c0e8147e856916eee89c6cea539574a105aa Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 16:37:50 -0700 Subject: [PATCH 2/6] fix(mcp_server_manager.py): ensure only allowed params sent to MCP server --- .../mcp_server/mcp_server_manager.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 129a756e4f5..8070bedbc79 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -603,9 +603,9 @@ class MCPServerManager: return tool_name not in server.disallowed_tools return True - def filter_allowed_params( + def validate_allowed_params( self, tool_name: str, arguments: Dict[str, Any], server: MCPServer - ) -> Dict[str, Any]: + ) -> None: """ Filter arguments to only include allowed parameters for the given tool. @@ -626,7 +626,7 @@ class MCPServerManager: # If no allowed_params configured, return all arguments if not server.allowed_params: - return arguments + return # Get the unprefixed tool name to match against config unprefixed_tool_name, _ = get_server_name_prefix_tool_mcp(tool_name) @@ -638,7 +638,7 @@ class MCPServerManager: # If this tool doesn't have allowed_params specified, allow all params if allowed_params_list is None: - return arguments + return None # Filter arguments to only include allowed parameters disallowed_params = [ @@ -655,8 +655,6 @@ class MCPServerManager: }, ) - return {k: v for k, v in arguments.items() if k in allowed_params_list} - async def check_tool_permission_for_key_team( self, tool_name: str, @@ -725,14 +723,11 @@ class MCPServerManager: ) ## filter parameters based on allowed_params configuration - filtered_arguments = self.filter_allowed_params( + self.validate_allowed_params( tool_name=name, arguments=arguments, server=server, ) - # Update arguments with filtered version - arguments.clear() - arguments.update(filtered_arguments) pre_hook_kwargs = { "name": name, From 0b27c361fe2a9fdf55e12fc1093a79cdca2c3632 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 17:33:11 -0700 Subject: [PATCH 3/6] feat(mcp/): initial commit raising correct oauth error --- .../mcp_server/auth/user_api_key_auth_mcp.py | 89 +++++++++++++------ .../proxy/_experimental/mcp_server/server.py | 41 +++++---- .../index.html} | 0 .../proxy/_experimental/out/onboarding.html | 1 - litellm/proxy/_new_secret_config.yaml | 26 ++---- litellm/proxy/proxy_server.py | 30 ++++--- 6 files changed, 108 insertions(+), 79 deletions(-) rename litellm/proxy/_experimental/out/{model_hub_table.html => model_hub_table/index.html} (100%) delete mode 100644 litellm/proxy/_experimental/out/onboarding.html diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index 0c15138b05b..714f2c87465 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -5,7 +5,12 @@ from starlette.requests import Request from starlette.types import Scope from litellm._logging import verbose_logger -from litellm.proxy._types import LiteLLM_TeamTable, SpecialHeaders, UserAPIKeyAuth +from litellm.proxy._types import ( + LiteLLM_TeamTable, + LitellmUserRoles, + SpecialHeaders, + UserAPIKeyAuth, +) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth @@ -109,10 +114,21 @@ class MCPRequestHandler: request.body = mock_body # type: ignore if ".well-known" in str(request.url): # public routes validated_user_api_key_auth = UserAPIKeyAuth() + elif litellm_api_key == "": + from fastapi import HTTPException + + raise HTTPException( + status_code=401, + detail="LiteLLM API key is missing. Please add it or use OAuth authentication.", + headers={ + "WWW-Authenticate": f'Bearer resource_metadata=f"{request.base_url}/.well-known/oauth-protected-resource"', + }, + ) else: validated_user_api_key_auth = await user_api_key_auth( api_key=litellm_api_key, request=request ) + return ( validated_user_api_key_auth, mcp_auth_header, @@ -344,14 +360,14 @@ class MCPRequestHandler: proxy_logging_obj, user_api_key_cache, ) - + if not user_api_key_auth: return None - + # Already loaded if user_api_key_auth.object_permission: return user_api_key_auth.object_permission - + # Need to fetch from DB if user_api_key_auth.object_permission_id and prisma_client: return await get_object_permission( @@ -361,7 +377,7 @@ class MCPRequestHandler: parent_otel_span=user_api_key_auth.parent_otel_span, proxy_logging_obj=proxy_logging_obj, ) - + return None @staticmethod @@ -369,16 +385,19 @@ class MCPRequestHandler: user_api_key_auth: Optional[UserAPIKeyAuth] = None, ): """Helper to get team object_permission from cache or DB.""" - from litellm.proxy.auth.auth_checks import get_object_permission, get_team_object + from litellm.proxy.auth.auth_checks import ( + get_object_permission, + get_team_object, + ) from litellm.proxy.proxy_server import ( prisma_client, proxy_logging_obj, user_api_key_cache, ) - + if not user_api_key_auth or not user_api_key_auth.team_id or not prisma_client: return None - + # First get the team object (which may have object_permission already loaded) team_obj: Optional[LiteLLM_TeamTable] = await get_team_object( team_id=user_api_key_auth.team_id, @@ -387,14 +406,14 @@ class MCPRequestHandler: parent_otel_span=user_api_key_auth.parent_otel_span, proxy_logging_obj=proxy_logging_obj, ) - + if not team_obj: return None - + # Already loaded if team_obj.object_permission: return team_obj.object_permission - + # Need to fetch from DB using object_permission_id if team_obj.object_permission_id: return await get_object_permission( @@ -404,7 +423,7 @@ class MCPRequestHandler: parent_otel_span=user_api_key_auth.parent_otel_span, proxy_logging_obj=proxy_logging_obj, ) - + return None @staticmethod @@ -415,26 +434,38 @@ class MCPRequestHandler: """ Get list of allowed tool names for a specific server based on key/team permissions. Follows same inheritance logic as get_allowed_mcp_servers. - + Args: server_id: Server ID to check permissions for user_api_key_auth: User auth - + Returns: List[str] if restrictions exist, None if no restrictions (allow all) """ if not user_api_key_auth: return None - + try: # Get key and team object permissions - key_obj_perm = await MCPRequestHandler._get_key_object_permission(user_api_key_auth) - team_obj_perm = await MCPRequestHandler._get_team_object_permission(user_api_key_auth) - + key_obj_perm = await MCPRequestHandler._get_key_object_permission( + user_api_key_auth + ) + team_obj_perm = await MCPRequestHandler._get_team_object_permission( + user_api_key_auth + ) + # Extract tool permissions for this server - key_tools = key_obj_perm.mcp_tool_permissions.get(server_id) if key_obj_perm and key_obj_perm.mcp_tool_permissions else None - team_tools = team_obj_perm.mcp_tool_permissions.get(server_id) if team_obj_perm and team_obj_perm.mcp_tool_permissions else None - + key_tools = ( + key_obj_perm.mcp_tool_permissions.get(server_id) + if key_obj_perm and key_obj_perm.mcp_tool_permissions + else None + ) + team_tools = ( + team_obj_perm.mcp_tool_permissions.get(server_id) + if team_obj_perm and team_obj_perm.mcp_tool_permissions + else None + ) + # Apply same inheritance logic as get_allowed_mcp_servers if team_tools: if key_tools: @@ -446,7 +477,7 @@ class MCPRequestHandler: else: # No team restrictions → use key restrictions return key_tools - + except Exception as e: verbose_logger.warning(f"Failed to get allowed tools for server: {str(e)}") return None @@ -459,12 +490,12 @@ class MCPRequestHandler: ) -> bool: """ Check if a specific tool is allowed for a server based on key/team permissions. - + Args: tool_name: Name of the tool to check server_id: Server ID user_api_key_auth: User auth - + Returns: True if allowed, False if blocked """ @@ -472,15 +503,15 @@ class MCPRequestHandler: server_id=server_id, user_api_key_auth=user_api_key_auth, ) - + # None means no restrictions (allow all) if allowed_tools is None: return True - + # Empty list means no tools allowed if not allowed_tools: return False - + # Check if tool is in allowed list return tool_name in allowed_tools @@ -555,7 +586,7 @@ class MCPRequestHandler: ) -> List[str]: """ Get allowed MCP servers for a team. - + Uses the helper _get_team_object_permission which: 1. First checks if object_permission is already loaded on the team 2. If not, fetches from DB using object_permission_id if it exists @@ -571,7 +602,7 @@ class MCPRequestHandler: object_permissions = await MCPRequestHandler._get_team_object_permission( user_api_key_auth ) - + if object_permissions is None: return [] diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index d7ebfb805f3..db3958d1566 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -364,25 +364,25 @@ if MCP_AVAILABLE: def _tool_name_matches(tool_name: str, filter_list: List[str]) -> bool: """ Check if a tool name matches any name in the filter list. - + Checks both the full tool name and unprefixed version (without server prefix). This allows users to configure simple tool names regardless of prefixing. - + Args: tool_name: The tool name to check (may be prefixed like "server-tool_name") filter_list: List of tool names to match against - + Returns: True if the tool name (prefixed or unprefixed) is in the filter list """ from litellm.proxy._experimental.mcp_server.utils import ( get_server_name_prefix_tool_mcp, ) - + # Check if the full name is in the list if tool_name in filter_list: return True - + # Check if the unprefixed name is in the list unprefixed_name, _ = get_server_name_prefix_tool_mcp(tool_name) return unprefixed_name in filter_list @@ -393,34 +393,36 @@ if MCP_AVAILABLE: ) -> List[MCPTool]: """ Filter tools by allowed/disallowed tools configuration. - + If allowed_tools is set, only tools in that list are returned. If disallowed_tools is set, tools in that list are excluded. Tool names are matched with and without server prefixes for flexibility. - + Args: tools: List of tools to filter mcp_server: Server configuration with allowed_tools/disallowed_tools - + Returns: Filtered list of tools """ tools_to_return = tools - + # Filter by allowed_tools (whitelist) if mcp_server.allowed_tools: tools_to_return = [ - tool for tool in tools + tool + for tool in tools if _tool_name_matches(tool.name, mcp_server.allowed_tools) ] - + # Filter by disallowed_tools (blacklist) if mcp_server.disallowed_tools: tools_to_return = [ - tool for tool in tools_to_return + tool + for tool in tools_to_return if not _tool_name_matches(tool.name, mcp_server.disallowed_tools) ] - + return tools_to_return async def _get_tools_from_mcp_servers( @@ -497,17 +499,17 @@ if MCP_AVAILABLE: extra_headers=extra_headers, add_prefix=add_prefix, ) - + filtered_tools = filter_tools_by_allowed_tools(tools, server) - + filtered_tools = await filter_tools_by_key_team_permissions( tools=filtered_tools, server_id=server_id, user_api_key_auth=user_api_key_auth, ) - + all_tools.extend(filtered_tools) - + verbose_logger.debug( f"Successfully fetched {len(tools)} tools from server {server.name}, {len(filtered_tools)} after filtering" ) @@ -529,7 +531,7 @@ if MCP_AVAILABLE: ) -> List[MCPTool]: """ Filter tools based on key/team mcp_tool_permissions. - + Note: Tool names in the DB are stored without server prefixes, but tool names from MCP servers are prefixed. We need to strip the prefix before comparing. @@ -551,7 +553,7 @@ if MCP_AVAILABLE: else: # No restrictions, return all tools filtered_tools = tools - + return filtered_tools async def _list_mcp_tools( @@ -906,6 +908,7 @@ if MCP_AVAILABLE: await session_manager.handle_request(scope, receive, send) except Exception as e: + raise e verbose_logger.exception(f"Error handling MCP request: {e}") # Instead of re-raising, try to send a graceful error response try: diff --git a/litellm/proxy/_experimental/out/model_hub_table.html b/litellm/proxy/_experimental/out/model_hub_table/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model_hub_table.html rename to litellm/proxy/_experimental/out/model_hub_table/index.html diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html deleted file mode 100644 index 04c6c886768..00000000000 --- a/litellm/proxy/_experimental/out/onboarding.html +++ /dev/null @@ -1 +0,0 @@ -LiteLLM Dashboard \ No newline at end of file diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index b2f4635ac99..ff686844168 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -17,23 +17,15 @@ model_list: api_key: dummy mcp_servers: - my_api_mcp: - url: "http://0.0.0.0:8090" - spec_path: "/Users/krrishdholakia/Documents/temp_py_folder/example_openapi.json" - auth_type: none - allowed_tools: ["getpetbyid", "my_api_mcp-findpetsbystatus"] - # Configure allowed parameters per tool - # Key: tool name (with or without prefix) - # Value: list of allowed parameter names - allowed_params: - # Using unprefixed tool name - "getpetbyid": ["petId"] - # Using prefixed tool name (both formats work) - "my_api_mcp-findpetsbystatus": ["status", "limit"] - # Example: allow only specific params for another tool - # "another_tool": ["param1", "param2"] - - + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: oauth2 + authorization_url: https://github.com/login/oauth/authorize + token_url: https://github.com/login/oauth/access_token + client_id: os.environ/GITHUB_OAUTH_CLIENT_ID + client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET + scopes: ["public_repo", "user:email"] + litellm_settings: callbacks: ["prometheus"] custom_prometheus_metadata_labels: ["metadata.initiative", "metadata.business-unit"] diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 7a1e8f1e735..366d3f57dc0 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1883,7 +1883,9 @@ class ProxyConfig: elif key == "priority_reservation_settings": from litellm.types.utils import PriorityReservationSettings - litellm.priority_reservation_settings = PriorityReservationSettings(**value) + litellm.priority_reservation_settings = PriorityReservationSettings( + **value + ) elif key == "callbacks": initialize_callbacks_on_proxy( value=value, @@ -2966,32 +2968,32 @@ class ProxyConfig: ) -> bool: """ Check if an object type should be loaded from the database based on general_settings.supported_db_objects. - + Args: object_type: Type of object to check (e.g., SupportedDBObjectType.MODELS, "models", etc.) - + Returns: True if the object should be loaded, False otherwise """ global general_settings - + # Get the supported_db_objects configuration supported_db_objects = general_settings.get("supported_db_objects", None) - + # If supported_db_objects is not set, load all objects (default behavior) if supported_db_objects is None: return True - + # If supported_db_objects is set, only load specified objects if not isinstance(supported_db_objects, list): verbose_proxy_logger.warning( f"supported_db_objects is not a list, got {type(supported_db_objects)}. Loading all objects." ) return True - + # Convert object_type to string for comparison (handles both str and enum) object_type_str = str(object_type) - + # Check if the object type is in the list (supports both str and enum values) return any(str(obj) == object_type_str for obj in supported_db_objects) @@ -3063,19 +3065,19 @@ class ProxyConfig: """ if self._should_load_db_object(object_type="guardrails"): await self._init_guardrails_in_db(prisma_client=prisma_client) - + if self._should_load_db_object(object_type="vector_stores"): await self._init_vector_stores_in_db(prisma_client=prisma_client) - + if self._should_load_db_object(object_type="mcp"): await self._init_mcp_servers_in_db() - + if self._should_load_db_object(object_type="pass_through_endpoints"): await self._init_pass_through_endpoints_in_db() - + if self._should_load_db_object(object_type="prompts"): await self._init_prompts_in_db(prisma_client=prisma_client) - + if self._should_load_db_object(object_type="model_cost_map"): await self._check_and_reload_model_cost_map(prisma_client=prisma_client) @@ -9708,6 +9710,8 @@ async def dynamic_mcp_route(mcp_server_name: str, request: Request): media_type=headers_dict.get("content-type", "application/json"), ) + except HTTPException as e: + raise e except Exception as e: verbose_proxy_logger.error( f"Error handling dynamic MCP route for {mcp_server_name}: {str(e)}" From c97495c5d9f9a7356230f4bff6fb4c50a7467f82 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 17:39:42 -0700 Subject: [PATCH 4/6] docs(mcp.md): document new allow/disallow tool parameters functionality --- docs/my-website/docs/mcp.md | 115 +++++++++++++++++++++++++- litellm/proxy/_new_secret_config.yaml | 33 ++++++-- 2 files changed, 138 insertions(+), 10 deletions(-) diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index 9365b0a5542..bb0759598af 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -246,7 +246,7 @@ litellm_settings: -## MCP Tool Filtering +## Allow/Disallow MCP Tools Control which tools are available from your MCP servers. You can either allow only specific tools or block dangerous ones. @@ -306,6 +306,119 @@ mcp_servers: - If you specify both `allowed_tools` and `disallowed_tools`, the allowed list takes priority - Tool names are case-sensitive +--- + +## Allow/Disallow MCP Tool Parameters + +Control which parameters are allowed for specific MCP tools using the `allowed_params` configuration. This provides fine-grained control over tool usage by restricting the parameters that can be passed to each tool. + +### Configuration + +`allowed_params` is a dictionary that maps tool names to lists of allowed parameter names. When configured, only the specified parameters will be accepted for that tool - any other parameters will be rejected with a 403 error. + +```yaml title="config.yaml with allowed_params" showLineNumbers +mcp_servers: + deepwiki_mcp: + url: https://mcp.deepwiki.com/mcp + transport: "http" + auth_type: "none" + allowed_params: + # Tool name: list of allowed parameters + read_wiki_contents: ["status"] + + my_api_mcp: + url: "https://my-api-server.com" + auth_type: "api_key" + auth_value: "my-key" + allowed_params: + # Using unprefixed tool name + getpetbyid: ["status"] + # Using prefixed tool name (both formats work) + my_api_mcp-findpetsbystatus: ["status", "limit"] + # Another tool with multiple allowed params + create_issue: ["title", "body", "labels"] +``` + +### How It Works + +1. **Tool-specific filtering**: Each tool can have its own list of allowed parameters +2. **Flexible naming**: Tool names can be specified with or without the server prefix (e.g., both `"getpetbyid"` and `"my_api_mcp-getpetbyid"` work) +3. **Whitelist approach**: Only parameters in the allowed list are permitted +4. **Unlisted tools**: If `allowed_params` is not set, all parameters are allowed +5. **Error handling**: Requests with disallowed parameters receive a 403 error with details about which parameters are allowed + +### Example Request Behavior + +With the configuration above, here's how requests would be handled: + +**✅ Allowed Request:** +```json +{ + "tool": "read_wiki_contents", + "arguments": { + "status": "active" + } +} +``` + +**❌ Rejected Request:** +```json +{ + "tool": "read_wiki_contents", + "arguments": { + "status": "active", + "limit": 10 // This parameter is not allowed + } +} +``` + +**Error Response:** +```json +{ + "error": "Parameters ['limit'] are not allowed for tool read_wiki_contents. Allowed parameters: ['status']. Contact proxy admin to allow these parameters." +} +``` + +### Use Cases + +- **Security**: Prevent users from accessing sensitive parameters or dangerous operations +- **Cost control**: Restrict expensive parameters (e.g., limiting result counts) +- **Compliance**: Enforce parameter usage policies for regulatory requirements +- **Staged rollouts**: Gradually enable parameters as tools are tested +- **Multi-tenant isolation**: Different parameter access for different user groups + +### Combining with Tool Filtering + +`allowed_params` works alongside `allowed_tools` and `disallowed_tools` for complete control: + +```yaml title="Combined filtering example" showLineNumbers +mcp_servers: + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: oauth2 + authorization_url: https://github.com/login/oauth/authorize + token_url: https://github.com/login/oauth/access_token + client_id: os.environ/GITHUB_OAUTH_CLIENT_ID + client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET + scopes: ["public_repo", "user:email"] + # Only allow specific tools + allowed_tools: ["create_issue", "list_issues", "search_issues"] + # Block dangerous operations + disallowed_tools: ["delete_repo"] + # Restrict parameters per tool + allowed_params: + create_issue: ["title", "body", "labels"] + list_issues: ["state", "sort", "perPage"] + search_issues: ["query", "sort", "order", "perPage"] +``` + +This configuration ensures that: +1. Only the three listed tools are available +2. The `delete_repo` tool is explicitly blocked +3. Each tool can only use its specified parameters + +--- + ## MCP Server Access Control LiteLLM Proxy provides two methods for controlling access to specific MCP servers: diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index ff686844168..996df3d3f56 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -17,15 +17,30 @@ model_list: api_key: dummy mcp_servers: - github_mcp: - url: "https://api.githubcopilot.com/mcp" - auth_type: oauth2 - authorization_url: https://github.com/login/oauth/authorize - token_url: https://github.com/login/oauth/access_token - client_id: os.environ/GITHUB_OAUTH_CLIENT_ID - client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET - scopes: ["public_repo", "user:email"] - + deepwiki_mcp: + url: https://mcp.deepwiki.com/mcp + transport: "http" + auth_type: "none" + allowed_params: + read_wiki_contents: ["status"] + + # my_api_mcp: + # url: "http://0.0.0.0:8090" + # spec_path: "/Users/krrishdholakia/Documents/temp_py_folder/example_openapi.json" + # auth_type: none + # allowed_tools: ["getpetbyid", "my_api_mcp-findpetsbystatus"] + # # Configure allowed parameters per tool + # # Key: tool name (with or without prefix) + # # Value: list of allowed parameter names + # allowed_params: + # # Using unprefixed tool name + # "getpetbyid": ["status"] + # # Using prefixed tool name (both formats work) + # "my_api_mcp-findpetsbystatus": ["status", "limit"] + # # Example: allow only specific params for another tool + # # "another_tool": ["param1", "param2"] + + litellm_settings: callbacks: ["prometheus"] custom_prometheus_metadata_labels: ["metadata.initiative", "metadata.business-unit"] From 37fdb486d4e4a0278e38190e5f62ec0fc6b46abc Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 19:06:19 -0700 Subject: [PATCH 5/6] refactor: comment out oauth error raising logic for now --- .../mcp_server/auth/user_api_key_auth_mcp.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index 714f2c87465..20a632f1674 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -114,16 +114,16 @@ class MCPRequestHandler: request.body = mock_body # type: ignore if ".well-known" in str(request.url): # public routes validated_user_api_key_auth = UserAPIKeyAuth() - elif litellm_api_key == "": - from fastapi import HTTPException + # elif litellm_api_key == "": + # from fastapi import HTTPException - raise HTTPException( - status_code=401, - detail="LiteLLM API key is missing. Please add it or use OAuth authentication.", - headers={ - "WWW-Authenticate": f'Bearer resource_metadata=f"{request.base_url}/.well-known/oauth-protected-resource"', - }, - ) + # raise HTTPException( + # status_code=401, + # detail="LiteLLM API key is missing. Please add it or use OAuth authentication.", + # headers={ + # "WWW-Authenticate": f'Bearer resource_metadata=f"{request.base_url}/.well-known/oauth-protected-resource"', + # }, + # ) else: validated_user_api_key_auth = await user_api_key_auth( api_key=litellm_api_key, request=request From 5be198248bc11fc29e93aac8bc164a5a9f057a41 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 8 Oct 2025 19:07:11 -0700 Subject: [PATCH 6/6] fix: fix linting error --- .../_experimental/mcp_server/auth/user_api_key_auth_mcp.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index 20a632f1674..e77ad11fae4 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -5,12 +5,7 @@ from starlette.requests import Request from starlette.types import Scope from litellm._logging import verbose_logger -from litellm.proxy._types import ( - LiteLLM_TeamTable, - LitellmUserRoles, - SpecialHeaders, - UserAPIKeyAuth, -) +from litellm.proxy._types import LiteLLM_TeamTable, SpecialHeaders, UserAPIKeyAuth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth