Skip to content

fix(mcp): clear allowed_tools and tool overrides on MCP server edit - #29411

Merged
mateo-berri merged 15 commits into
litellm_internal_stagingfrom
litellm_mcp_clear_allowed_tools
Jun 2, 2026
Merged

fix(mcp): clear allowed_tools and tool overrides on MCP server edit#29411
mateo-berri merged 15 commits into
litellm_internal_stagingfrom
litellm_mcp_clear_allowed_tools

Conversation

@Sameerlite

@Sameerlite Sameerlite commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Dashboard sends [] / {} instead of null when clearing MCP tool allowlists and name overrides.
  • Backend coerces legacy null on partial update before Prisma (allowed_tools, tool_name_to_display_name, tool_name_to_description).
  • Edit UI honors an empty stored allowlist instead of auto-selecting all tools on reload.
    Fixes LIT-3424
image

Note

Medium Risk
Changes MCP tool access rules and partial-update persistence; misconfiguration could block or expose tools, but behavior is covered by new tests and targets a known edit bug.

Overview
Fixes MCP server tool allowlist and override persistence so clearing or tightening restrictions in the dashboard matches what the proxy enforces.

The dashboard now sends tool_allowlist_enforced in mcp_info, uses [] / {} instead of null when clearing allowlists and display/description maps, and tracks user interaction so legacy servers without a stored whitelist still show as unrestricted until the user explicitly restricts tools. Partial updates coerce legacy null to [] or {} before Prisma writes.

On the proxy, server_applies_tool_allowlist centralizes when whitelist filtering runs: enforced empty allowlists block all tools and calls, while servers without the flag and with an empty list keep legacy “allow all” behavior. Tool listing, REST tool fetch, and check_allowed_or_banned_tools share that logic.

Reviewed by Cursor Bugbot for commit 6290c4b. Bugbot is set up for automated code reviews on this repo. Configure here.

Send empty arrays/objects from the dashboard instead of null, coerce legacy
null payloads before Prisma, and stop auto-selecting all tools when the
stored allowlist is empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.84211% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/_experimental/mcp_server/utils.py 77.27% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes MCP tool allowlist and name-override persistence so that clearing or tightening restrictions in the dashboard matches what the proxy enforces. The core changes introduce a tool_allowlist_enforced flag in mcp_info, a server_applies_tool_allowlist helper that distinguishes enforced-empty (block all) from legacy-empty (allow all), and backend null-to-empty coercion for partial updates.

  • Backend (db.py, utils.py, mcp_server_manager.py, server.py, rest_endpoints.py): Null allowed_tools / JSON-map fields from the UI are coerced to [] / {} before Prisma writes; server_applies_tool_allowlist centralises the enforcement decision; the REST listing endpoint now always invokes filter_tools_by_allowed_tools (previously it skipped disallowed-tool filtering when no allowlist was set).
  • Frontend (mcp_server_edit.tsx, mcp_tool_configuration.tsx, create_mcp_server.tsx): A hasToolAllowlistInteraction flag tracks whether the user has made any tool-selection change; legacy servers display as fully enabled (via effectiveAllowedTools) until the admin explicitly restricts tools; the save payload sends tool_allowlist_enforced and [] / {} instead of null so the backend can distinguish "enforced empty" from "not set".

Confidence Score: 4/5

Safe to merge with awareness of the disallowed_tools enforcement change in the REST path and the increased complexity of the frontend state machine.

The backend logic is sound and well-tested. The REST endpoint now unconditionally calls filter_tools_by_allowed_tools, which for the first time enforces disallowed_tools on the REST listing path; any deployment relying on that gap will see behavior change. The frontend introduces a multi-flag state machine (hasToolAllowlistInteraction, isLegacyUnrestrictedEdit, effectiveAllowedTools) across two components, making the interaction ordering complex and harder to reason about in untested edge cases. No concrete data-loss or security defect was identified, but the surface is large enough to merit careful review before merging.

mcp_server_edit.tsx and mcp_tool_configuration.tsx — the interaction between hasToolAllowlistInteraction, existingAllowedTools, and the auto-init ref across asynchronous tool-load and OAuth-restore flows is intricate and warrants close attention.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/utils.py Adds server_applies_tool_allowlist / is_server_tool_allowlist_enforced helpers; correctly handles dict, JSON-string, and None mcp_info; legacy empty-list behaviour preserved.
litellm/proxy/_experimental/mcp_server/db.py Null-to-empty coercion for allowed_tools / JSON map fields on partial update; JSON serialisation now keyed on field presence (not falsiness), handling explicit empty-dict clears correctly.
litellm/proxy/_experimental/mcp_server/server.py filter_tools_by_allowed_tools now uses server_applies_tool_allowlist; enforced-empty allowlist correctly returns [] while legacy-empty falls through to disallowed_tools filter.
litellm/proxy/_experimental/mcp_server/rest_endpoints.py REST listing now unconditionally calls filter_tools_by_allowed_tools, aligning disallowed_tools enforcement with the SSE/HTTP path — intentional behaviour change described in PR.
ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx Introduces hasToolAllowlistInteraction flag and existingAllowedTools derivation; complex multi-effect state machine; previous-thread race conditions appear addressed by new interaction tracking.
ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx Adds isEditMode / hasToolAllowlistInteraction props; effectiveAllowedTools shows legacy servers as fully enabled until user interacts; handleAllowedToolsChange notifies parent on first interaction.
ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx Now always sends allowed_tools: [] and empty maps on create instead of null; tool_allowlist_enforced only true when user has interacted or tools selected — correctly preserves legacy allow-all behaviour.
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py Good new unit tests for enforced-empty vs legacy-empty allowlist, and for check_allowed_or_banned_tools; existing test fixed to pin MagicMock allowed_tools/disallowed_tools to None.
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py Two new tests verify null-to-empty coercion for allowed_tools and JSON map fields on partial update.
tests/mcp_tests/test_mcp_server.py New mock-based test validates that disallowed_tools is enforced via the REST listing path even without an allowlist; existing test corrected for MagicMock truthiness.

Reviews (8): Last reviewed commit: "fix(mcp): enforce disallowed_tools on RE..." | Re-trigger Greptile

Comment thread ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx Outdated
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread litellm/proxy/_experimental/mcp_server/db.py
@veria-ai

veria-ai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 2 · PR risk: 0/10

Set mcp_info.tool_allowlist_enforced on UI save so [] blocks all tools
while legacy servers with default [] remain unrestricted.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Sameerlite

Copy link
Copy Markdown
Contributor Author

@greptileai

@Sameerlite

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Legacy server edit silently blocks all tools
    • The edit UI now treats empty legacy allowlists as unrestricted unless the allowlist was explicitly enforced, with regression coverage for enforced empty lists.

You can send follow-ups to the cloud agent here.

Comment thread ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx Outdated
@CLAassistant

CLAassistant commented Jun 1, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ Sameerlite
✅ mateo-berri
❌ cursoragent
You have signed the CLA already but the status is still pending? Let us recheck it.

@Sameerlite

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9e3e48b. Configure here.

MagicMock auto-attributes are truthy and trigger server_applies_tool_allowlist
after the empty-allowlist enforcement change.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Sameerlite

Copy link
Copy Markdown
Contributor Author

bugbot run

@Sameerlite

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit cc7b45a. Configure here.

Sameerlite and others added 3 commits June 1, 2026 18:28
Only set tool_allowlist_enforced when already enforced or the user
selected tools; skip allowlist fields on save for unrestricted servers;
do not auto-select all tools when editing legacy servers before load.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx Outdated
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
@Sameerlite

Copy link
Copy Markdown
Contributor Author

@greptileai

@Sameerlite

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Unused variable wasAllowlistEnforced is dead code
    • Removed the unused wasAllowlistEnforced assignment from the MCP server edit submit handler.
Preview (9601bd6133)
diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py
--- a/litellm/proxy/_experimental/mcp_server/db.py
+++ b/litellm/proxy/_experimental/mcp_server/db.py
@@ -67,6 +67,17 @@
         # ``alias=None`` is a valid request to clear the stored alias.
         if data_dict.get("alias") is None and "alias" not in fields_set:
             data_dict.pop("alias", None)
+        # Prisma ``allowed_tools`` is a required String[]; ``null`` is invalid.
+        # The UI sends null to clear a whitelist — treat that as ``[]``.
+        if "allowed_tools" in data_dict and data_dict["allowed_tools"] is None:
+            data_dict["allowed_tools"] = []
+        # Json map fields use ``@default("{}")``; explicit null means clear overrides.
+        for json_map_field in (
+            "tool_name_to_display_name",
+            "tool_name_to_description",
+        ):
+            if json_map_field in data_dict and data_dict[json_map_field] is None:
+                data_dict[json_map_field] = {}
     else:
         data_dict = data.model_dump(exclude_none=True)
         # Ensure alias is always present in the dict (even if None)
@@ -93,13 +104,13 @@
     if data_dict.get("env") is not None:
         data_dict["env"] = safe_dumps(data_dict["env"])
 
-    if data_dict.get("tool_name_to_display_name") is not None:
+    if "tool_name_to_display_name" in data_dict:
         data_dict["tool_name_to_display_name"] = safe_dumps(
-            data_dict["tool_name_to_display_name"]
+            data_dict["tool_name_to_display_name"] or {}
         )
-    if data_dict.get("tool_name_to_description") is not None:
+    if "tool_name_to_description" in data_dict:
         data_dict["tool_name_to_description"] = safe_dumps(
-            data_dict["tool_name_to_description"]
+            data_dict["tool_name_to_description"] or {}
         )
 
     # mcp_access_groups is already List[str], no serialization needed

diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
--- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
+++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
@@ -2429,7 +2429,13 @@
         """
         Check if the tool is allowed or banned for the given server
         """
-        if server.allowed_tools:
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
+        if server_applies_tool_allowlist(server):
+            if not server.allowed_tools:
+                return False
             return (
                 tool_name in server.allowed_tools
                 or f"{server.name}-{tool_name}" in server.allowed_tools

diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
--- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
+++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
@@ -366,8 +366,11 @@
         )
 
         # Filter tools based on allowed_tools configuration
-        # Only filter if allowed_tools is explicitly configured (not None and not empty)
-        if server.allowed_tools is not None and len(server.allowed_tools) > 0:
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
+        if server_applies_tool_allowlist(server):
             tools = filter_tools_by_allowed_tools(tools, server)
 
         # Filter tools based on user_api_key_auth.object_permission.mcp_tool_permissions

diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py
--- a/litellm/proxy/_experimental/mcp_server/server.py
+++ b/litellm/proxy/_experimental/mcp_server/server.py
@@ -945,10 +945,16 @@
         Returns:
             Filtered list of tools
         """
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
         tools_to_return = tools
 
         # Filter by allowed_tools (whitelist)
-        if mcp_server.allowed_tools:
+        if server_applies_tool_allowlist(mcp_server):
+            if not mcp_server.allowed_tools:
+                return []
             tools_to_return = [
                 tool
                 for tool in tools

diff --git a/litellm/proxy/_experimental/mcp_server/utils.py b/litellm/proxy/_experimental/mcp_server/utils.py
--- a/litellm/proxy/_experimental/mcp_server/utils.py
+++ b/litellm/proxy/_experimental/mcp_server/utils.py
@@ -2,6 +2,7 @@
 MCP Server Utilities
 """
 
+import json
 import re
 from typing import Any, Dict, Iterator, Mapping, Optional, Tuple, Union
 
@@ -162,6 +163,36 @@
     return None
 
 
+MCP_TOOL_ALLOWLIST_ENFORCED_KEY = "tool_allowlist_enforced"
+
+
+def _parse_mcp_info_dict(mcp_info: Any) -> Optional[Dict[str, Any]]:
+    if mcp_info is None:
+        return None
+    if isinstance(mcp_info, dict):
+        return mcp_info
+    if isinstance(mcp_info, str):
+        try:
+            parsed = json.loads(mcp_info)
+        except (ValueError, TypeError):
+            return None
+        return parsed if isinstance(parsed, dict) else None
+    return None
+
+
+def is_server_tool_allowlist_enforced(mcp_server: Any) -> bool:
+    mcp_info = _parse_mcp_info_dict(getattr(mcp_server, "mcp_info", None))
+    if not mcp_info:
+        return False
+    return bool(mcp_info.get(MCP_TOOL_ALLOWLIST_ENFORCED_KEY))
+
+
+def server_applies_tool_allowlist(mcp_server: Any) -> bool:
+    """Whether server-level allowed_tools whitelist filtering is active."""
+    allowed_tools = getattr(mcp_server, "allowed_tools", None) or []
+    return is_server_tool_allowlist_enforced(mcp_server) or bool(allowed_tools)
+
+
 def validate_and_normalize_mcp_server_payload(payload: Any) -> None:
     """
     Validate and normalize MCP server payload fields (server_name and alias).

diff --git a/tests/mcp_tests/test_mcp_server.py b/tests/mcp_tests/test_mcp_server.py
--- a/tests/mcp_tests/test_mcp_server.py
+++ b/tests/mcp_tests/test_mcp_server.py
@@ -1862,9 +1862,11 @@
     )
     from mcp.types import Tool as MCPTool
 
-    # Create a mock server
+    # Create a mock server (pin allowlist fields; MagicMock auto-attrs are truthy)
     mock_server = MagicMock()
     mock_server.mcp_info = {"server_name": "zapier"}
+    mock_server.allowed_tools = None
+    mock_server.disallowed_tools = None
 
     # Create mock tools
     mock_tools = [

diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
@@ -69,6 +69,34 @@
 
 
 @pytest.mark.asyncio
+async def test_partial_update_null_tool_name_maps_clear_to_empty_json():
+    """Explicit null on Json map fields must clear overrides (UI legacy)."""
+    data = UpdateMCPServerRequest(
+        server_id="my-test-server",
+        tool_name_to_display_name=None,
+        tool_name_to_description=None,
+    )
+
+    data_dict = await _run_update(data)
+
+    assert data_dict["tool_name_to_display_name"] == "{}"
+    assert data_dict["tool_name_to_description"] == "{}"
+
+
+@pytest.mark.asyncio
+async def test_partial_update_null_allowed_tools_clears_whitelist():
+    """Explicit null must clear the whitelist (UI legacy); Prisma requires []."""
+    data = UpdateMCPServerRequest(
+        server_id="my-test-server",
+        allowed_tools=None,
+    )
+
+    data_dict = await _run_update(data)
+
+    assert data_dict["allowed_tools"] == []
+
+
+@pytest.mark.asyncio
 async def test_partial_update_preserves_http_transport():
     """The reported prod incident: a PUT without transport must not flip http->sse."""
     data = UpdateMCPServerRequest(

diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
@@ -4184,6 +4184,85 @@
     assert len(filtered_tools) == 2
 
 
+def test_filter_tools_enforced_empty_allowlist_blocks_all():
+    from mcp.types import Tool
+
+    from litellm.proxy._experimental.mcp_server.server import (
+        filter_tools_by_allowed_tools,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    tools = [
+        Tool(
+            name="read_wiki_structure",
+            title=None,
+            description="",
+            inputSchema={"type": "object"},
+            outputSchema=None,
+            annotations=None,
+        ),
+    ]
+    server = MCPServer(
+        server_id="deepwiki",
+        name="deepwiki",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info={"tool_allowlist_enforced": True},
+    )
+
+    assert filter_tools_by_allowed_tools(tools, server) == []
+
+
+def test_filter_tools_legacy_empty_allowlist_allows_all():
+    from mcp.types import Tool
+
+    from litellm.proxy._experimental.mcp_server.server import (
+        filter_tools_by_allowed_tools,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    tools = [
+        Tool(
+            name="read_wiki_structure",
+            title=None,
+            description="",
+            inputSchema={"type": "object"},
+            outputSchema=None,
+            annotations=None,
+        ),
+    ]
+    server = MCPServer(
+        server_id="legacy",
+        name="legacy",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info=None,
+    )
+
+    assert len(filter_tools_by_allowed_tools(tools, server)) == 1
+
+
+def test_check_allowed_or_banned_tools_enforced_empty_denies_calls():
+    from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
+        MCPServerManager,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    manager = MCPServerManager.__new__(MCPServerManager)
+    server = MCPServer(
+        server_id="deepwiki",
+        name="deepwiki",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info={"tool_allowlist_enforced": True},
+    )
+
+    assert manager.check_allowed_or_banned_tools("read_wiki_structure", server) is False
+
+
 @pytest.mark.asyncio
 async def test_get_tools_from_mcp_servers_injects_stored_oauth2_token():
     """
@@ -4540,10 +4619,10 @@
                 await global_mcp_server_manager._ensure_upstream_initialize_instructions_cached(
                     server
                 )
-                assert create.await_count == 1, (
-                    "Second probe within cooldown must not reconnect to upstream"
-                )
                 assert (
+                    create.await_count == 1
+                ), "Second probe within cooldown must not reconnect to upstream"
+                assert (
                     "empty-server"
                     not in global_mcp_server_manager._upstream_initialize_instructions_by_server_id
                 )
@@ -4567,7 +4646,9 @@
 
         server = _make_instruction_server(server_id="boom-server", instructions=None)
         fake_client = MagicMock()
-        fake_client.run_with_session = AsyncMock(side_effect=RuntimeError("upstream down"))
+        fake_client.run_with_session = AsyncMock(
+            side_effect=RuntimeError("upstream down")
+        )
         fake_client._last_initialize_instructions = None
 
         create = AsyncMock(return_value=fake_client)
@@ -4579,10 +4660,10 @@
                 await global_mcp_server_manager._ensure_upstream_initialize_instructions_cached(
                     server
                 )
-                assert create.await_count == 1, (
-                    "Second probe within cooldown must not reconnect after failure"
-                )
                 assert (
+                    create.await_count == 1
+                ), "Second probe within cooldown must not reconnect after failure"
+                assert (
                     "boom-server"
                     not in global_mcp_server_manager._upstream_initialize_instructions_by_server_id
                 )

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
@@ -384,12 +384,13 @@
           description: restValues.description,
           logo_url: logoUrl || undefined,
           mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
+          tool_allowlist_enforced: allowedTools.length > 0,
         },
         mcp_access_groups: accessGroups,
         alias: restValues.alias,
-        allowed_tools: allowedTools.length > 0 ? allowedTools : null,
-        tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null,
-        tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null,
+        allowed_tools: allowedTools,
+        tool_name_to_display_name: toolNameToDisplayName,
+        tool_name_to_description: toolNameToDescription,
         allow_all_keys: Boolean(allowAllKeysRaw),
         available_on_public_internet: Boolean(availableOnPublicInternetRaw),
         delegate_auth_to_upstream: Boolean(delegateAuthToUpstreamRaw),

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
@@ -35,7 +35,9 @@
 }));
 
 vi.mock("./mcp_tool_configuration", () => ({
-  default: () => <div data-testid="mcp-tool-config" />,
+  default: ({ existingAllowedTools }: { existingAllowedTools: string[] | null }) => (
+    <div data-testid="mcp-tool-config" data-existing-allowed-tools={JSON.stringify(existingAllowedTools)} />
+  ),
 }));
 
 // ── fixtures ──────────────────────────────────────────────────────────────────
@@ -218,6 +220,48 @@
   });
 });
 
+describe("MCPServerEdit (tool allowlist)", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("treats legacy empty allowed_tools as unrestricted", () => {
+    render(
+      <MCPServerEdit
+        mcpServer={{
+          ...interactiveOAuthServer,
+          allowed_tools: [],
+          mcp_info: { server_name: "OAuthServer" },
+        }}
+        accessToken={null}
+        onCancel={vi.fn()}
+        onSuccess={vi.fn()}
+        availableAccessGroups={[]}
+      />,
+    );
+
+    expect(screen.getByTestId("mcp-tool-config")).toHaveAttribute("data-existing-allowed-tools", "null");
+  });
+
+  it("honors enforced empty allowed_tools", () => {
+    render(
+      <MCPServerEdit
+        mcpServer={{
+          ...interactiveOAuthServer,
+          allowed_tools: [],
+          mcp_info: { server_name: "OAuthServer", tool_allowlist_enforced: true },
+        }}
+        accessToken={null}
+        onCancel={vi.fn()}
+        onSuccess={vi.fn()}
+        availableAccessGroups={[]}
+      />,
+    );
+
+    expect(screen.getByTestId("mcp-tool-config")).toHaveAttribute("data-existing-allowed-tools", "[]");
+  });
+});
+
 describe("MCPServerEdit (interactive OAuth)", () => {
   beforeEach(() => {
     vi.clearAllMocks();

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
@@ -68,6 +68,10 @@
   const currentAuthorizationUrl = Form.useWatch("authorization_url", form);
   const currentTokenUrl = Form.useWatch("token_url", form);
   const currentRegistrationUrl = Form.useWatch("registration_url", form);
+  const hasExistingToolAllowlist =
+    Boolean(mcpServer.mcp_info?.tool_allowlist_enforced) ||
+    (mcpServer.allowed_tools?.length ?? 0) > 0;
+  const existingAllowedTools = hasExistingToolAllowlist ? (mcpServer.allowed_tools ?? []) : null;
 
   const persistEditUiState = () => {
     if (typeof window === "undefined") {
@@ -208,12 +212,12 @@
 
   // Initialize allowed tools and tool overrides from existing server data
   useEffect(() => {
-    if (mcpServer.allowed_tools) {
-      setAllowedTools(mcpServer.allowed_tools);
+    if (hasExistingToolAllowlist) {
+      setAllowedTools(mcpServer.allowed_tools ?? []);
     }
     setToolNameToDisplayName(mcpServer.tool_name_to_display_name ?? {});
     setToolNameToDescription(mcpServer.tool_name_to_description ?? {});
-  }, [mcpServer]);
+  }, [mcpServer, hasExistingToolAllowlist]);
 
   useEffect(() => {
     if (typeof window === "undefined") {
@@ -529,6 +533,8 @@
         mcpServer.alias ||
         "unknown";
 
+      const toolAllowlistEnforced = hasExistingToolAllowlist || allowedTools.length > 0;
+
       const payload: Record<string, any> = {
         ...restValues,
         ...stdioFields,
@@ -537,18 +543,24 @@
         env_json: undefined,
         server_id: mcpServer.server_id,
         mcp_info: {
+          ...(mcpServer.mcp_info ?? {}),
           server_name: mcpInfoServerName,
           description: restValues.description,
           logo_url: logoUrl || undefined,
           mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
+          tool_allowlist_enforced: toolAllowlistEnforced,
         },
         mcp_access_groups: accessGroups,
         alias: restValues.alias,
         // Include permission management fields
         extra_headers: restValues.extra_headers || [],
-        allowed_tools: allowedTools.length > 0 ? allowedTools : null,
-        tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null,
-        tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null,
+        ...(toolAllowlistEnforced
+          ? {
+              allowed_tools: allowedTools,
+              tool_name_to_display_name: toolNameToDisplayName,
+              tool_name_to_description: toolNameToDescription,
+            }
+          : {}),
         disallowed_tools: restValues.disallowed_tools || [],
         static_headers: staticHeaders,
         allow_all_keys: Boolean(allowAllKeysRaw ?? mcpServer.allow_all_keys),
@@ -1106,7 +1118,8 @@
                   registration_url: currentRegistrationUrl ?? mcpServer.registration_url,
                 }}
                 allowedTools={allowedTools}
-                existingAllowedTools={mcpServer.allowed_tools || null}
+                existingAllowedTools={existingAllowedTools}
+                isEditMode
                 onAllowedToolsChange={setAllowedTools}
                 toolNameToDisplayName={toolNameToDisplayName}
                 toolNameToDescription={toolNameToDescription}

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx
@@ -28,6 +28,8 @@
   externalIsLoading?: boolean;
   externalError?: string | null;
   externalCanFetch?: boolean;
+  /** When true, do not auto-select all tools for servers with no stored allowlist. */
+  isEditMode?: boolean;
 }
 
 interface ToolEntry {
@@ -158,6 +160,7 @@
   externalIsLoading,
   externalError,
   externalCanFetch,
+  isEditMode = false,
 }) => {
   const previousToolsRef = useRef<ToolEntry[]>([]);
   const [toolSearchTerm, setToolSearchTerm] = useState("");
@@ -258,12 +261,15 @@
       if (!hasInitializedRef.current) {
         hasInitializedRef.current = true;
 
-        if (existingAllowedTools && existingAllowedTools.length > 0) {
-          // Edit mode: pre-select tools that match existing allowed tools
+        if (existingAllowedTools !== null) {
+          // Edit mode: honor stored allowlist, including [] (user cleared all tools).
           const validExistingTools = existingAllowedTools.filter((toolName) =>
             availableToolNames.includes(toolName)
           );
           onAllowedToolsChange(validExistingTools);
+        } else if (isEditMode) {
+          // Unrestricted legacy server — do not auto-select before the user chooses tools.
+          onAllowedToolsChange([]);
         } else if (suggestedTools.length > 0) {
           // OpenAPI preset: only enable suggested tools by default
           onAllowedToolsChange(
@@ -467,7 +473,11 @@
               <McpCrudPermissionPanel
                 tools={tools}
                 searchFilter={toolSearchTerm}
-                value={allowedTools.length === 0 ? undefined : allowedTools}
+                value={
+                  existingAllowedTools === null && allowedTools.length === 0
+                    ? undefined
+                    : allowedTools
+                }
                 onChange={(allowed) => onAllowedToolsChange(allowed)}
               />
             )}

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
@@ -105,6 +105,7 @@
   description?: string;
   logo_url?: string;
   mcp_server_cost_info?: MCPServerCostInfo | null;
+  tool_allowlist_enforced?: boolean;
 }
 
 // Define the structure for a single MCP tool

You can send follow-ups to the cloud agent here.

Comment thread ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx Outdated
@Sameerlite

Copy link
Copy Markdown
Contributor Author

bugbot run

@Sameerlite

Copy link
Copy Markdown
Contributor Author

@greptileai

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Flat and CRUD views show contradictory tool states
    • Legacy unrestricted edit mode now uses the effective allow-all tool set for flat rendering and toggles, matching the CRUD view semantics.
Preview (65597980cc)
diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py
--- a/litellm/proxy/_experimental/mcp_server/db.py
+++ b/litellm/proxy/_experimental/mcp_server/db.py
@@ -67,6 +67,17 @@
         # ``alias=None`` is a valid request to clear the stored alias.
         if data_dict.get("alias") is None and "alias" not in fields_set:
             data_dict.pop("alias", None)
+        # Prisma ``allowed_tools`` is a required String[]; ``null`` is invalid.
+        # The UI sends null to clear a whitelist — treat that as ``[]``.
+        if "allowed_tools" in data_dict and data_dict["allowed_tools"] is None:
+            data_dict["allowed_tools"] = []
+        # Json map fields use ``@default("{}")``; explicit null means clear overrides.
+        for json_map_field in (
+            "tool_name_to_display_name",
+            "tool_name_to_description",
+        ):
+            if json_map_field in data_dict and data_dict[json_map_field] is None:
+                data_dict[json_map_field] = {}
     else:
         data_dict = data.model_dump(exclude_none=True)
         # Ensure alias is always present in the dict (even if None)
@@ -93,13 +104,13 @@
     if data_dict.get("env") is not None:
         data_dict["env"] = safe_dumps(data_dict["env"])
 
-    if data_dict.get("tool_name_to_display_name") is not None:
+    if "tool_name_to_display_name" in data_dict:
         data_dict["tool_name_to_display_name"] = safe_dumps(
-            data_dict["tool_name_to_display_name"]
+            data_dict["tool_name_to_display_name"] or {}
         )
-    if data_dict.get("tool_name_to_description") is not None:
+    if "tool_name_to_description" in data_dict:
         data_dict["tool_name_to_description"] = safe_dumps(
-            data_dict["tool_name_to_description"]
+            data_dict["tool_name_to_description"] or {}
         )
 
     # mcp_access_groups is already List[str], no serialization needed

diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
--- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
+++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
@@ -2429,7 +2429,13 @@
         """
         Check if the tool is allowed or banned for the given server
         """
-        if server.allowed_tools:
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
+        if server_applies_tool_allowlist(server):
+            if not server.allowed_tools:
+                return False
             return (
                 tool_name in server.allowed_tools
                 or f"{server.name}-{tool_name}" in server.allowed_tools

diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
--- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
+++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
@@ -366,8 +366,11 @@
         )
 
         # Filter tools based on allowed_tools configuration
-        # Only filter if allowed_tools is explicitly configured (not None and not empty)
-        if server.allowed_tools is not None and len(server.allowed_tools) > 0:
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
+        if server_applies_tool_allowlist(server):
             tools = filter_tools_by_allowed_tools(tools, server)
 
         # Filter tools based on user_api_key_auth.object_permission.mcp_tool_permissions

diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py
--- a/litellm/proxy/_experimental/mcp_server/server.py
+++ b/litellm/proxy/_experimental/mcp_server/server.py
@@ -945,10 +945,16 @@
         Returns:
             Filtered list of tools
         """
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
         tools_to_return = tools
 
         # Filter by allowed_tools (whitelist)
-        if mcp_server.allowed_tools:
+        if server_applies_tool_allowlist(mcp_server):
+            if not mcp_server.allowed_tools:
+                return []
             tools_to_return = [
                 tool
                 for tool in tools

diff --git a/litellm/proxy/_experimental/mcp_server/utils.py b/litellm/proxy/_experimental/mcp_server/utils.py
--- a/litellm/proxy/_experimental/mcp_server/utils.py
+++ b/litellm/proxy/_experimental/mcp_server/utils.py
@@ -2,6 +2,7 @@
 MCP Server Utilities
 """
 
+import json
 import re
 from typing import Any, Dict, Iterator, Mapping, Optional, Tuple, Union
 
@@ -162,6 +163,36 @@
     return None
 
 
+MCP_TOOL_ALLOWLIST_ENFORCED_KEY = "tool_allowlist_enforced"
+
+
+def _parse_mcp_info_dict(mcp_info: Any) -> Optional[Dict[str, Any]]:
+    if mcp_info is None:
+        return None
+    if isinstance(mcp_info, dict):
+        return mcp_info
+    if isinstance(mcp_info, str):
+        try:
+            parsed = json.loads(mcp_info)
+        except (ValueError, TypeError):
+            return None
+        return parsed if isinstance(parsed, dict) else None
+    return None
+
+
+def is_server_tool_allowlist_enforced(mcp_server: Any) -> bool:
+    mcp_info = _parse_mcp_info_dict(getattr(mcp_server, "mcp_info", None))
+    if not mcp_info:
+        return False
+    return bool(mcp_info.get(MCP_TOOL_ALLOWLIST_ENFORCED_KEY))
+
+
+def server_applies_tool_allowlist(mcp_server: Any) -> bool:
+    """Whether server-level allowed_tools whitelist filtering is active."""
+    allowed_tools = getattr(mcp_server, "allowed_tools", None) or []
+    return is_server_tool_allowlist_enforced(mcp_server) or bool(allowed_tools)
+
+
 def validate_and_normalize_mcp_server_payload(payload: Any) -> None:
     """
     Validate and normalize MCP server payload fields (server_name and alias).

diff --git a/tests/mcp_tests/test_mcp_server.py b/tests/mcp_tests/test_mcp_server.py
--- a/tests/mcp_tests/test_mcp_server.py
+++ b/tests/mcp_tests/test_mcp_server.py
@@ -1862,9 +1862,11 @@
     )
     from mcp.types import Tool as MCPTool
 
-    # Create a mock server
+    # Create a mock server (pin allowlist fields; MagicMock auto-attrs are truthy)
     mock_server = MagicMock()
     mock_server.mcp_info = {"server_name": "zapier"}
+    mock_server.allowed_tools = None
+    mock_server.disallowed_tools = None
 
     # Create mock tools
     mock_tools = [

diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
@@ -69,6 +69,34 @@
 
 
 @pytest.mark.asyncio
+async def test_partial_update_null_tool_name_maps_clear_to_empty_json():
+    """Explicit null on Json map fields must clear overrides (UI legacy)."""
+    data = UpdateMCPServerRequest(
+        server_id="my-test-server",
+        tool_name_to_display_name=None,
+        tool_name_to_description=None,
+    )
+
+    data_dict = await _run_update(data)
+
+    assert data_dict["tool_name_to_display_name"] == "{}"
+    assert data_dict["tool_name_to_description"] == "{}"
+
+
+@pytest.mark.asyncio
+async def test_partial_update_null_allowed_tools_clears_whitelist():
+    """Explicit null must clear the whitelist (UI legacy); Prisma requires []."""
+    data = UpdateMCPServerRequest(
+        server_id="my-test-server",
+        allowed_tools=None,
+    )
+
+    data_dict = await _run_update(data)
+
+    assert data_dict["allowed_tools"] == []
+
+
+@pytest.mark.asyncio
 async def test_partial_update_preserves_http_transport():
     """The reported prod incident: a PUT without transport must not flip http->sse."""
     data = UpdateMCPServerRequest(

diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
@@ -4184,6 +4184,85 @@
     assert len(filtered_tools) == 2
 
 
+def test_filter_tools_enforced_empty_allowlist_blocks_all():
+    from mcp.types import Tool
+
+    from litellm.proxy._experimental.mcp_server.server import (
+        filter_tools_by_allowed_tools,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    tools = [
+        Tool(
+            name="read_wiki_structure",
+            title=None,
+            description="",
+            inputSchema={"type": "object"},
+            outputSchema=None,
+            annotations=None,
+        ),
+    ]
+    server = MCPServer(
+        server_id="deepwiki",
+        name="deepwiki",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info={"tool_allowlist_enforced": True},
+    )
+
+    assert filter_tools_by_allowed_tools(tools, server) == []
+
+
+def test_filter_tools_legacy_empty_allowlist_allows_all():
+    from mcp.types import Tool
+
+    from litellm.proxy._experimental.mcp_server.server import (
+        filter_tools_by_allowed_tools,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    tools = [
+        Tool(
+            name="read_wiki_structure",
+            title=None,
+            description="",
+            inputSchema={"type": "object"},
+            outputSchema=None,
+            annotations=None,
+        ),
+    ]
+    server = MCPServer(
+        server_id="legacy",
+        name="legacy",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info=None,
+    )
+
+    assert len(filter_tools_by_allowed_tools(tools, server)) == 1
+
+
+def test_check_allowed_or_banned_tools_enforced_empty_denies_calls():
+    from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
+        MCPServerManager,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    manager = MCPServerManager.__new__(MCPServerManager)
+    server = MCPServer(
+        server_id="deepwiki",
+        name="deepwiki",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info={"tool_allowlist_enforced": True},
+    )
+
+    assert manager.check_allowed_or_banned_tools("read_wiki_structure", server) is False
+
+
 @pytest.mark.asyncio
 async def test_get_tools_from_mcp_servers_injects_stored_oauth2_token():
     """
@@ -4540,10 +4619,10 @@
                 await global_mcp_server_manager._ensure_upstream_initialize_instructions_cached(
                     server
                 )
-                assert create.await_count == 1, (
-                    "Second probe within cooldown must not reconnect to upstream"
-                )
                 assert (
+                    create.await_count == 1
+                ), "Second probe within cooldown must not reconnect to upstream"
+                assert (
                     "empty-server"
                     not in global_mcp_server_manager._upstream_initialize_instructions_by_server_id
                 )
@@ -4567,7 +4646,9 @@
 
         server = _make_instruction_server(server_id="boom-server", instructions=None)
         fake_client = MagicMock()
-        fake_client.run_with_session = AsyncMock(side_effect=RuntimeError("upstream down"))
+        fake_client.run_with_session = AsyncMock(
+            side_effect=RuntimeError("upstream down")
+        )
         fake_client._last_initialize_instructions = None
 
         create = AsyncMock(return_value=fake_client)
@@ -4579,10 +4660,10 @@
                 await global_mcp_server_manager._ensure_upstream_initialize_instructions_cached(
                     server
                 )
-                assert create.await_count == 1, (
-                    "Second probe within cooldown must not reconnect after failure"
-                )
                 assert (
+                    create.await_count == 1
+                ), "Second probe within cooldown must not reconnect after failure"
+                assert (
                     "boom-server"
                     not in global_mcp_server_manager._upstream_initialize_instructions_by_server_id
                 )

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
@@ -384,12 +384,13 @@
           description: restValues.description,
           logo_url: logoUrl || undefined,
           mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
+          tool_allowlist_enforced: allowedTools.length > 0,
         },
         mcp_access_groups: accessGroups,
         alias: restValues.alias,
-        allowed_tools: allowedTools.length > 0 ? allowedTools : null,
-        tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null,
-        tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null,
+        allowed_tools: allowedTools,
+        tool_name_to_display_name: toolNameToDisplayName,
+        tool_name_to_description: toolNameToDescription,
         allow_all_keys: Boolean(allowAllKeysRaw),
         available_on_public_internet: Boolean(availableOnPublicInternetRaw),
         delegate_auth_to_upstream: Boolean(delegateAuthToUpstreamRaw),

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
@@ -35,7 +35,9 @@
 }));
 
 vi.mock("./mcp_tool_configuration", () => ({
-  default: () => <div data-testid="mcp-tool-config" />,
+  default: ({ existingAllowedTools }: { existingAllowedTools: string[] | null }) => (
+    <div data-testid="mcp-tool-config" data-existing-allowed-tools={JSON.stringify(existingAllowedTools)} />
+  ),
 }));
 
 // ── fixtures ──────────────────────────────────────────────────────────────────
@@ -218,6 +220,48 @@
   });
 });
 
+describe("MCPServerEdit (tool allowlist)", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("treats legacy empty allowed_tools as unrestricted", () => {
+    render(
+      <MCPServerEdit
+        mcpServer={{
+          ...interactiveOAuthServer,
+          allowed_tools: [],
+          mcp_info: { server_name: "OAuthServer" },
+        }}
+        accessToken={null}
+        onCancel={vi.fn()}
+        onSuccess={vi.fn()}
+        availableAccessGroups={[]}
+      />,
+    );
+
+    expect(screen.getByTestId("mcp-tool-config")).toHaveAttribute("data-existing-allowed-tools", "null");
+  });
+
+  it("honors enforced empty allowed_tools", () => {
+    render(
+      <MCPServerEdit
+        mcpServer={{
+          ...interactiveOAuthServer,
+          allowed_tools: [],
+          mcp_info: { server_name: "OAuthServer", tool_allowlist_enforced: true },
+        }}
+        accessToken={null}
+        onCancel={vi.fn()}
+        onSuccess={vi.fn()}
+        availableAccessGroups={[]}
+      />,
+    );
+
+    expect(screen.getByTestId("mcp-tool-config")).toHaveAttribute("data-existing-allowed-tools", "[]");
+  });
+});
+
 describe("MCPServerEdit (interactive OAuth)", () => {
   beforeEach(() => {
     vi.clearAllMocks();

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
@@ -68,6 +68,10 @@
   const currentAuthorizationUrl = Form.useWatch("authorization_url", form);
   const currentTokenUrl = Form.useWatch("token_url", form);
   const currentRegistrationUrl = Form.useWatch("registration_url", form);
+  const hasExistingToolAllowlist =
+    Boolean(mcpServer.mcp_info?.tool_allowlist_enforced) ||
+    (mcpServer.allowed_tools?.length ?? 0) > 0;
+  const existingAllowedTools = hasExistingToolAllowlist ? (mcpServer.allowed_tools ?? []) : null;
 
   const persistEditUiState = () => {
     if (typeof window === "undefined") {
@@ -208,12 +212,12 @@
 
   // Initialize allowed tools and tool overrides from existing server data
   useEffect(() => {
-    if (mcpServer.allowed_tools) {
-      setAllowedTools(mcpServer.allowed_tools);
+    if (hasExistingToolAllowlist) {
+      setAllowedTools(mcpServer.allowed_tools ?? []);
     }
     setToolNameToDisplayName(mcpServer.tool_name_to_display_name ?? {});
     setToolNameToDescription(mcpServer.tool_name_to_description ?? {});
-  }, [mcpServer]);
+  }, [mcpServer, hasExistingToolAllowlist]);
 
   useEffect(() => {
     if (typeof window === "undefined") {
@@ -529,6 +533,8 @@
         mcpServer.alias ||
         "unknown";
 
+      const toolAllowlistEnforced = hasExistingToolAllowlist || allowedTools.length > 0;
+
       const payload: Record<string, any> = {
         ...restValues,
         ...stdioFields,
@@ -537,18 +543,24 @@
         env_json: undefined,
         server_id: mcpServer.server_id,
         mcp_info: {
+          ...(mcpServer.mcp_info ?? {}),
           server_name: mcpInfoServerName,
           description: restValues.description,
           logo_url: logoUrl || undefined,
           mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
+          tool_allowlist_enforced: toolAllowlistEnforced,
         },
         mcp_access_groups: accessGroups,
         alias: restValues.alias,
         // Include permission management fields
         extra_headers: restValues.extra_headers || [],
-        allowed_tools: allowedTools.length > 0 ? allowedTools : null,
-        tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null,
-        tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null,
+        ...(toolAllowlistEnforced
+          ? {
+              allowed_tools: allowedTools,
+              tool_name_to_display_name: toolNameToDisplayName,
+              tool_name_to_description: toolNameToDescription,
+            }
+          : {}),
         disallowed_tools: restValues.disallowed_tools || [],
         static_headers: staticHeaders,
         allow_all_keys: Boolean(allowAllKeysRaw ?? mcpServer.allow_all_keys),
@@ -1106,7 +1118,8 @@
                   registration_url: currentRegistrationUrl ?? mcpServer.registration_url,
                 }}
                 allowedTools={allowedTools}
-                existingAllowedTools={mcpServer.allowed_tools || null}
+                existingAllowedTools={existingAllowedTools}
+                isEditMode
                 onAllowedToolsChange={setAllowedTools}
                 toolNameToDisplayName={toolNameToDisplayName}
                 toolNameToDescription={toolNameToDescription}

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.test.tsx
new file mode 100644
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.test.tsx
@@ -1,0 +1,46 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import MCPToolConfiguration from "./mcp_tool_configuration";
+
+const tools = [
+  { name: "read_user", description: "Read user" },
+  { name: "delete_user", description: "Delete user" },
+];
+
+const renderToolConfiguration = (onAllowedToolsChange = vi.fn()) => {
+  render(
+    <MCPToolConfiguration
+      accessToken="token"
+      formValues={{ url: "https://example.com/mcp", transport: "http", auth_type: "none" }}
+      allowedTools={[]}
+      existingAllowedTools={null}
+      onAllowedToolsChange={onAllowedToolsChange}
+      toolNameToDisplayName={{}}
+      toolNameToDescription={{}}
+      onToolNameToDisplayNameChange={vi.fn()}
+      onToolNameToDescriptionChange={vi.fn()}
+      externalTools={tools}
+      externalCanFetch
+      isEditMode
+    />,
+  );
+
+  return onAllowedToolsChange;
+};
+
+describe("MCPToolConfiguration", () => {
+  it("shows legacy unrestricted edit tools enabled in flat view", async () => {
+    const onAllowedToolsChange = renderToolConfiguration();
+
+    fireEvent.click(screen.getByText("Flat List"));
+
+    expect(screen.getByText("2 of 2 tools enabled for user access")).toBeInTheDocument();
+    expect(screen.getAllByText("Enabled")).toHaveLength(2);
+
+    fireEvent.click(screen.getByText("read_user"));
+
+    await waitFor(() => {
+      expect(onAllowedToolsChange).toHaveBeenLastCalledWith(["delete_user"]);
+    });
+  });
+});

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx
@@ -28,6 +28,8 @@
   externalIsLoading?: boolean;
   externalError?: string | null;
   externalCanFetch?: boolean;
+  /** When true, do not auto-select all tools for servers with no stored allowlist. */
+  isEditMode?: boolean;
 }
 
 interface ToolEntry {
@@ -158,6 +160,7 @@
   externalIsLoading,
   externalError,
   externalCanFetch,
+  isEditMode = false,
 }) => {
   const previousToolsRef = useRef<ToolEntry[]>([]);
   const [toolSearchTerm, setToolSearchTerm] = useState("");
@@ -258,12 +261,15 @@
       if (!hasInitializedRef.current) {
         hasInitializedRef.current = true;
 
-        if (existingAllowedTools && existingAllowedTools.length > 0) {
-          // Edit mode: pre-select tools that match existing allowed tools
+        if (existingAllowedTools !== null) {
+          // Edit mode: honor stored allowlist, including [] (user cleared all tools).
           const validExistingTools = existingAllowedTools.filter((toolName) =>
             availableToolNames.includes(toolName)
           );
           onAllowedToolsChange(validExistingTools);
+        } else if (isEditMode) {
+          // Unrestricted legacy server — do not auto-select before the user chooses tools.
+          onAllowedToolsChange([]);
         } else if (suggestedTools.length > 0) {
           // OpenAPI preset: only enable suggested tools by default
           onAllowedToolsChange(
@@ -283,11 +289,18 @@
     previousToolsRef.current = tools;
   }, [tools, allowedTools, existingAllowedTools, onAllowedToolsChange, suggestedTools]);
 
+  const isLegacyUnrestrictedEdit = isEditMode && existingAllowedTools === null && allowedTools.length === 0;
+  const effectiveAllowedTools = useMemo(
+    () => (isLegacyUnrestrictedEdit ? tools.map((tool) => tool.name) : allowedTools),
+    [allowedTools, isLegacyUnrestrictedEdit, tools]
+  );
+  const effectiveAllowedToolNames = useMemo(() => new Set(effectiveAllowedTools), [effectiveAllowedTools]);
+
   const handleToolToggle = (toolName: string) => {
-    if (allowedTools.includes(toolName)) {
-      onAllowedToolsChange(allowedTools.filter((name) => name !== toolName));
+    if (effectiveAllowedToolNames.has(toolName)) {
+      onAllowedToolsChange(effectiveAllowedTools.filter((name) => name !== toolName));
     } else {
-      onAllowedToolsChange([...allowedTools, toolName]);
+      onAllowedToolsChange([...effectiveAllowedTools, toolName]);
     }
   };
 
@@ -327,25 +340,27 @@
   const handleEnableSuggested = () => {
     // Enable ALL suggested tools (not just the currently filtered subset)
     const suggestedNames = suggestedTools.map((t) => t.name);
-    const others = allowedTools.filter((n) => !suggestedToolNames.has(n));
-    onAllowedToolsChange([...others, ...suggestedNames]);
+    const missingSuggestedNames = suggestedNames.filter((name) => !effectiveAllowedToolNames.has(name));
+    if (missingSuggestedNames.length === 0) return;
+    onAllowedToolsChange([...effectiveAllowedTools, ...missingSuggestedNames]);
   };
 
   const handleDisableSuggested = () => {
     // Disable ALL suggested tools (not just the currently filtered subset)
-    onAllowedToolsChange(allowedTools.filter((n) => !suggestedToolNames.has(n)));
+    onAllowedToolsChange(effectiveAllowedTools.filter((n) => !suggestedToolNames.has(n)));
   };
 
   const handleEnableRest = () => {
     // Enable ALL non-suggested tools (not just the currently filtered subset)
     const restNames = tools.filter((t) => !suggestedToolNames.has(t.name)).map((t) => t.name);
-    const current = new Set(allowedTools);
-    onAllowedToolsChange([...allowedTools, ...restNames.filter((n) => !current.has(n))]);
+    const missingRestNames = restNames.filter((n) => !effectiveAllowedToolNames.has(n));
+    if (missingRestNames.length === 0) return;
+    onAllowedToolsChange([...effectiveAllowedTools, ...missingRestNames]);
   };
 
   const handleDisableRest = () => {
     // Disable ALL non-suggested tools (not just the currently filtered subset)
-    onAllowedToolsChange(allowedTools.filter((n) => suggestedToolNames.has(n)));
+    onAllowedToolsChange(effectiveAllowedTools.filter((n) => suggestedToolNames.has(n)));
   };
 
   // Don't show anything if required fields aren't filled
@@ -446,8 +461,8 @@
             <div className="flex items-center gap-2 p-3 bg-green-50 rounded-lg border border-green-200">
               <CheckCircleOutlined className="text-green-600" />
               <Text className="text-green-700 font-medium">
-                {allowedTools.length} of {tools.length} {tools.length === 1 ? "tool" : "tools"} enabled for user
-                access
+                {effectiveAllowedTools.length} of {tools.length} {tools.length === 1 ? "tool" : "tools"} enabled for
+                user access
               </Text>
             </div>
 
@@ -467,7 +482,7 @@
               <McpCrudPermissionPanel
                 tools={tools}
                 searchFilter={toolSearchTerm}
-                value={allowedTools.length === 0 ? undefined : allowedTools}
+                value={isLegacyUnrestrictedEdit ? undefined : allowedTools}
                 onChange={(allowed) => onAllowedToolsChange(allowed)}
               />
             )}
@@ -509,7 +524,7 @@
                       <ToolRow
                         key={tool.name}
                         tool={tool}
-                        isEnabled={allowedTools.includes(tool.name)}
+                        isEnabled={effectiveAllowedToolNames.has(tool.name)}
                         isEditExpanded={expandedTools.has(tool.name)}
                         toolNameToDisplayName={toolNameToDisplayName}
                         toolNameToDescription={toolNameToDescription}
@@ -548,7 +563,7 @@
                       <ToolRow
                         key={tool.name}
                         tool={tool}
-                        isEnabled={allowedTools.includes(tool.name)}
+                        isEnabled={effectiveAllowedToolNames.has(tool.name)}
                         isEditExpanded={expandedTools.has(tool.name)}
                         toolNameToDisplayName={toolNameToDisplayName}
                         toolNameToDescription={toolNameToDescription}

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
@@ -105,6 +105,7 @@
   description?: string;
   logo_url?: string;
   mcp_server_cost_info?: MCPServerCostInfo | null;
+  tool_allowlist_enforced?: boolean;
 }
 
 // Define the structure for a single MCP tool

You can send follow-ups to the cloud agent here.

Comment thread ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx Outdated
@Sameerlite

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Legacy unrestricted flag snaps back when all tools disabled
    • Added a latched tool-allowlist interaction flag so legacy unrestricted edits no longer revert to all enabled and explicit empty allowlists are saved.
  • ✅ Fixed: Tool overrides silently dropped for legacy unrestricted servers
    • Tool display-name and description overrides are now included in edit payloads independently of allowlist enforcement.
Preview (6290c4b7bf)
diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py
--- a/litellm/proxy/_experimental/mcp_server/db.py
+++ b/litellm/proxy/_experimental/mcp_server/db.py
@@ -67,6 +67,17 @@
         # ``alias=None`` is a valid request to clear the stored alias.
         if data_dict.get("alias") is None and "alias" not in fields_set:
             data_dict.pop("alias", None)
+        # Prisma ``allowed_tools`` is a required String[]; ``null`` is invalid.
+        # The UI sends null to clear a whitelist — treat that as ``[]``.
+        if "allowed_tools" in data_dict and data_dict["allowed_tools"] is None:
+            data_dict["allowed_tools"] = []
+        # Json map fields use ``@default("{}")``; explicit null means clear overrides.
+        for json_map_field in (
+            "tool_name_to_display_name",
+            "tool_name_to_description",
+        ):
+            if json_map_field in data_dict and data_dict[json_map_field] is None:
+                data_dict[json_map_field] = {}
     else:
         data_dict = data.model_dump(exclude_none=True)
         # Ensure alias is always present in the dict (even if None)
@@ -93,13 +104,13 @@
     if data_dict.get("env") is not None:
         data_dict["env"] = safe_dumps(data_dict["env"])
 
-    if data_dict.get("tool_name_to_display_name") is not None:
+    if "tool_name_to_display_name" in data_dict:
         data_dict["tool_name_to_display_name"] = safe_dumps(
-            data_dict["tool_name_to_display_name"]
+            data_dict["tool_name_to_display_name"] or {}
         )
-    if data_dict.get("tool_name_to_description") is not None:
+    if "tool_name_to_description" in data_dict:
         data_dict["tool_name_to_description"] = safe_dumps(
-            data_dict["tool_name_to_description"]
+            data_dict["tool_name_to_description"] or {}
         )
 
     # mcp_access_groups is already List[str], no serialization needed

diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
--- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
+++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
@@ -2429,7 +2429,13 @@
         """
         Check if the tool is allowed or banned for the given server
         """
-        if server.allowed_tools:
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
+        if server_applies_tool_allowlist(server):
+            if not server.allowed_tools:
+                return False
             return (
                 tool_name in server.allowed_tools
                 or f"{server.name}-{tool_name}" in server.allowed_tools

diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
--- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
+++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
@@ -366,8 +366,11 @@
         )
 
         # Filter tools based on allowed_tools configuration
-        # Only filter if allowed_tools is explicitly configured (not None and not empty)
-        if server.allowed_tools is not None and len(server.allowed_tools) > 0:
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
+        if server_applies_tool_allowlist(server):
             tools = filter_tools_by_allowed_tools(tools, server)
 
         # Filter tools based on user_api_key_auth.object_permission.mcp_tool_permissions

diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py
--- a/litellm/proxy/_experimental/mcp_server/server.py
+++ b/litellm/proxy/_experimental/mcp_server/server.py
@@ -945,10 +945,16 @@
         Returns:
             Filtered list of tools
         """
+        from litellm.proxy._experimental.mcp_server.utils import (
+            server_applies_tool_allowlist,
+        )
+
         tools_to_return = tools
 
         # Filter by allowed_tools (whitelist)
-        if mcp_server.allowed_tools:
+        if server_applies_tool_allowlist(mcp_server):
+            if not mcp_server.allowed_tools:
+                return []
             tools_to_return = [
                 tool
                 for tool in tools

diff --git a/litellm/proxy/_experimental/mcp_server/utils.py b/litellm/proxy/_experimental/mcp_server/utils.py
--- a/litellm/proxy/_experimental/mcp_server/utils.py
+++ b/litellm/proxy/_experimental/mcp_server/utils.py
@@ -2,6 +2,7 @@
 MCP Server Utilities
 """
 
+import json
 import re
 from typing import Any, Dict, Iterator, Mapping, Optional, Tuple, Union
 
@@ -162,6 +163,36 @@
     return None
 
 
+MCP_TOOL_ALLOWLIST_ENFORCED_KEY = "tool_allowlist_enforced"
+
+
+def _parse_mcp_info_dict(mcp_info: Any) -> Optional[Dict[str, Any]]:
+    if mcp_info is None:
+        return None
+    if isinstance(mcp_info, dict):
+        return mcp_info
+    if isinstance(mcp_info, str):
+        try:
+            parsed = json.loads(mcp_info)
+        except (ValueError, TypeError):
+            return None
+        return parsed if isinstance(parsed, dict) else None
+    return None
+
+
+def is_server_tool_allowlist_enforced(mcp_server: Any) -> bool:
+    mcp_info = _parse_mcp_info_dict(getattr(mcp_server, "mcp_info", None))
+    if not mcp_info:
+        return False
+    return bool(mcp_info.get(MCP_TOOL_ALLOWLIST_ENFORCED_KEY))
+
+
+def server_applies_tool_allowlist(mcp_server: Any) -> bool:
+    """Whether server-level allowed_tools whitelist filtering is active."""
+    allowed_tools = getattr(mcp_server, "allowed_tools", None) or []
+    return is_server_tool_allowlist_enforced(mcp_server) or bool(allowed_tools)
+
+
 def validate_and_normalize_mcp_server_payload(payload: Any) -> None:
     """
     Validate and normalize MCP server payload fields (server_name and alias).

diff --git a/tests/mcp_tests/test_mcp_server.py b/tests/mcp_tests/test_mcp_server.py
--- a/tests/mcp_tests/test_mcp_server.py
+++ b/tests/mcp_tests/test_mcp_server.py
@@ -1862,9 +1862,11 @@
     )
     from mcp.types import Tool as MCPTool
 
-    # Create a mock server
+    # Create a mock server (pin allowlist fields; MagicMock auto-attrs are truthy)
     mock_server = MagicMock()
     mock_server.mcp_info = {"server_name": "zapier"}
+    mock_server.allowed_tools = None
+    mock_server.disallowed_tools = None
 
     # Create mock tools
     mock_tools = [

diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_partial_update.py
@@ -69,6 +69,34 @@
 
 
 @pytest.mark.asyncio
+async def test_partial_update_null_tool_name_maps_clear_to_empty_json():
+    """Explicit null on Json map fields must clear overrides (UI legacy)."""
+    data = UpdateMCPServerRequest(
+        server_id="my-test-server",
+        tool_name_to_display_name=None,
+        tool_name_to_description=None,
+    )
+
+    data_dict = await _run_update(data)
+
+    assert data_dict["tool_name_to_display_name"] == "{}"
+    assert data_dict["tool_name_to_description"] == "{}"
+
+
+@pytest.mark.asyncio
+async def test_partial_update_null_allowed_tools_clears_whitelist():
+    """Explicit null must clear the whitelist (UI legacy); Prisma requires []."""
+    data = UpdateMCPServerRequest(
+        server_id="my-test-server",
+        allowed_tools=None,
+    )
+
+    data_dict = await _run_update(data)
+
+    assert data_dict["allowed_tools"] == []
+
+
+@pytest.mark.asyncio
 async def test_partial_update_preserves_http_transport():
     """The reported prod incident: a PUT without transport must not flip http->sse."""
     data = UpdateMCPServerRequest(

diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
@@ -4184,6 +4184,85 @@
     assert len(filtered_tools) == 2
 
 
+def test_filter_tools_enforced_empty_allowlist_blocks_all():
+    from mcp.types import Tool
+
+    from litellm.proxy._experimental.mcp_server.server import (
+        filter_tools_by_allowed_tools,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    tools = [
+        Tool(
+            name="read_wiki_structure",
+            title=None,
+            description="",
+            inputSchema={"type": "object"},
+            outputSchema=None,
+            annotations=None,
+        ),
+    ]
+    server = MCPServer(
+        server_id="deepwiki",
+        name="deepwiki",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info={"tool_allowlist_enforced": True},
+    )
+
+    assert filter_tools_by_allowed_tools(tools, server) == []
+
+
+def test_filter_tools_legacy_empty_allowlist_allows_all():
+    from mcp.types import Tool
+
+    from litellm.proxy._experimental.mcp_server.server import (
+        filter_tools_by_allowed_tools,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    tools = [
+        Tool(
+            name="read_wiki_structure",
+            title=None,
+            description="",
+            inputSchema={"type": "object"},
+            outputSchema=None,
+            annotations=None,
+        ),
+    ]
+    server = MCPServer(
+        server_id="legacy",
+        name="legacy",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info=None,
+    )
+
+    assert len(filter_tools_by_allowed_tools(tools, server)) == 1
+
+
+def test_check_allowed_or_banned_tools_enforced_empty_denies_calls():
+    from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
+        MCPServerManager,
+    )
+    from litellm.types.mcp import MCPTransport
+    from litellm.types.mcp_server.mcp_server_manager import MCPServer
+
+    manager = MCPServerManager.__new__(MCPServerManager)
+    server = MCPServer(
+        server_id="deepwiki",
+        name="deepwiki",
+        transport=MCPTransport.http,
+        allowed_tools=[],
+        mcp_info={"tool_allowlist_enforced": True},
+    )
+
+    assert manager.check_allowed_or_banned_tools("read_wiki_structure", server) is False
+
+
 @pytest.mark.asyncio
 async def test_get_tools_from_mcp_servers_injects_stored_oauth2_token():
     """
@@ -4540,10 +4619,10 @@
                 await global_mcp_server_manager._ensure_upstream_initialize_instructions_cached(
                     server
                 )
-                assert create.await_count == 1, (
-                    "Second probe within cooldown must not reconnect to upstream"
-                )
                 assert (
+                    create.await_count == 1
+                ), "Second probe within cooldown must not reconnect to upstream"
+                assert (
                     "empty-server"
                     not in global_mcp_server_manager._upstream_initialize_instructions_by_server_id
                 )
@@ -4567,7 +4646,9 @@
 
         server = _make_instruction_server(server_id="boom-server", instructions=None)
         fake_client = MagicMock()
-        fake_client.run_with_session = AsyncMock(side_effect=RuntimeError("upstream down"))
+        fake_client.run_with_session = AsyncMock(
+            side_effect=RuntimeError("upstream down")
+        )
         fake_client._last_initialize_instructions = None
 
         create = AsyncMock(return_value=fake_client)
@@ -4579,10 +4660,10 @@
                 await global_mcp_server_manager._ensure_upstream_initialize_instructions_cached(
                     server
                 )
-                assert create.await_count == 1, (
-                    "Second probe within cooldown must not reconnect after failure"
-                )
                 assert (
+                    create.await_count == 1
+                ), "Second probe within cooldown must not reconnect after failure"
+                assert (
                     "boom-server"
                     not in global_mcp_server_manager._upstream_initialize_instructions_by_server_id
                 )

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
@@ -384,12 +384,13 @@
           description: restValues.description,
           logo_url: logoUrl || undefined,
           mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
+          tool_allowlist_enforced: allowedTools.length > 0,
         },
         mcp_access_groups: accessGroups,
         alias: restValues.alias,
-        allowed_tools: allowedTools.length > 0 ? allowedTools : null,
-        tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null,
-        tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null,
+        allowed_tools: allowedTools,
+        tool_name_to_display_name: toolNameToDisplayName,
+        tool_name_to_description: toolNameToDescription,
         allow_all_keys: Boolean(allowAllKeysRaw),
         available_on_public_internet: Boolean(availableOnPublicInternetRaw),
         delegate_auth_to_upstream: Boolean(delegateAuthToUpstreamRaw),

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
@@ -35,7 +35,34 @@
 }));
 
 vi.mock("./mcp_tool_configuration", () => ({
-  default: () => <div data-testid="mcp-tool-config" />,
+  default: ({
+    existingAllowedTools,
+    onAllowedToolsChange,
+    onToolAllowlistInteraction,
+    onToolNameToDisplayNameChange,
+    onToolNameToDescriptionChange,
+  }: any) => (
+    <div data-testid="mcp-tool-config" data-existing-allowed-tools={JSON.stringify(existingAllowedTools)}>
+      <button
+        type="button"
+        onClick={() => {
+          onToolAllowlistInteraction?.();
+          onAllowedToolsChange([]);
+        }}
+      >
+        Disable all tools
+      </button>
+      <button
+        type="button"
+        onClick={() => {
+          onToolNameToDisplayNameChange({ read_user: "Read User" });
+          onToolNameToDescriptionChange({ read_user: "Reads users" });
+        }}
+      >
+        Set tool overrides
+      </button>
+    </div>
+  ),
 }));
 
 // ── fixtures ──────────────────────────────────────────────────────────────────
@@ -43,7 +70,7 @@
 const interactiveOAuthServer = {
   server_id: "oauth_server_1",
   server_name: "OAuthServer",
-  alias: "oauth_server",   // underscores: hyphens fail validateMCPServerName
+  alias: "oauth_server", // underscores: hyphens fail validateMCPServerName
   description: "Interactive OAuth MCP server",
   transport: "http",
   url: "https://example.com/mcp",
@@ -218,6 +245,128 @@
   });
 });
 
+describe("MCPServerEdit (tool allowlist)", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("treats legacy empty allowed_tools as unrestricted", () => {
+    render(
+      <MCPServerEdit
+        mcpServer={{
+          ...interactiveOAuthServer,
+          allowed_tools: [],
+          mcp_info: { server_name: "OAuthServer" },
+        }}
+        accessToken={null}
+        onCancel={vi.fn()}
+        onSuccess={vi.fn()}
+        availableAccessGroups={[]}
+      />,
+    );
+
+    expect(screen.getByTestId("mcp-tool-config")).toHaveAttribute("data-existing-allowed-tools", "null");
+  });
+
+  it("honors enforced empty allowed_tools", () => {
+    render(
+      <MCPServerEdit
+        mcpServer={{
+          ...interactiveOAuthServer,
+          allowed_tools: [],
+          mcp_info: { server_name: "OAuthServer", tool_allowlist_enforced: true },
+        }}
+        accessToken={null}
+        onCancel={vi.fn()}
+        onSuccess={vi.fn()}
+        availableAccessGroups={[]}
+      />,
+    );
+
+    expect(screen.getByTestId("mcp-tool-config")).toHaveAttribute("data-existing-allowed-tools", "[]");
+  });
+
+  it("saves an explicit empty allowlist after legacy unrestricted tools are disabled", async () => {
+    vi.mocked(networking.updateMCPServer).mockResolvedValue({
+      ...interactiveOAuthServer,
+      allowed_tools: [],
+      mcp_info: { server_name: "OAuthServer", tool_allowlist_enforced: true },
+    });
+
+    render(
+      <MCPServerEdit
+        mcpServer={{
+          ...interactiveOAuthServer,
+          allowed_tools: [],
+          mcp_info: { server_name: "OAuthServer" },
+        }}
+        accessToken="access-token"
+        onCancel={vi.fn()}
+        onSuccess={vi.fn()}
+        availableAccessGroups={[]}
+      />,
+    );
+
+    await act(async () => {
+      fireEvent.click(screen.getByRole("button", { name: "Disable all tools" }));
+    });
+
+    const saveButtons = screen.getAllByRole("button", { name: "Save Changes" });
+    await act(async () => {
+      fireEvent.click(saveButtons[0]);
+    });
+
+    await waitFor(() => {
+      expect(networking.updateMCPServer).toHaveBeenCalledTimes(1);
+    });
+
+    const [, payload] = vi.mocked(networking.updateMCPServer).mock.calls[0];
+    expect(payload.mcp_info.tool_allowlist_enforced).toBe(true);
+    expect(payload.allowed_tools).toEqual([]);
+  });
+
+  it("saves tool overrides for legacy unrestricted servers", async () => {
+    vi.mocked(networking.updateMCPServer).mockResolvedValue({
+      ...interactiveOAuthServer,
+      tool_name_to_display_name: { read_user: "Read User" },
+      tool_name_to_description: { read_user: "Reads users" },
+    });
+
+    render(
+      <MCPServerEdit
+        mcpServer={{
+          ...interactiveOAuthServer,
+          allowed_tools: [],
+          mcp_info: { server_name: "OAuthServer" },
+        }}
+        accessToken="access-token"
+        onCancel={vi.fn()}
+        onSuccess={vi.fn()}
+        availableAccessGroups={[]}
+      />,
+    );
+
+    await act(async () => {
+      fireEvent.click(screen.getByRole("button", { name: "Set tool overrides" }));
+    });
+
+    const saveButtons = screen.getAllByRole("button", { name: "Save Changes" });
+    await act(async () => {
+      fireEvent.click(saveButtons[0]);
+    });
+
+    await waitFor(() => {
+      expect(networking.updateMCPServer).toHaveBeenCalledTimes(1);
+    });
+
+    const [, payload] = vi.mocked(networking.updateMCPServer).mock.calls[0];
+    expect(payload.mcp_info.tool_allowlist_enforced).toBe(false);
+    expect(payload.allowed_tools).toBeUndefined();
+    expect(payload.tool_name_to_display_name).toEqual({ read_user: "Read User" });
+    expect(payload.tool_name_to_description).toEqual({ read_user: "Reads users" });
+  });
+});
+
 describe("MCPServerEdit (interactive OAuth)", () => {
   beforeEach(() => {
     vi.clearAllMocks();

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
@@ -41,6 +41,7 @@
   const [searchValue, setSearchValue] = useState<string>("");
   const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false);
   const [allowedTools, setAllowedTools] = useState<string[]>([]);
+  const [hasToolAllowlistInteraction, setHasToolAllowlistInteraction] = useState(false);
   const [toolNameToDisplayName, setToolNameToDisplayName] = useState<Record<string, string>>({});
   const [toolNameToDescription, setToolNameToDescription] = useState<Record<string, string>>({});
   const [pendingRestoredValues, setPendingRestoredValues] = useState<Record<string, any> | null>(null);
@@ -68,6 +69,9 @@
   const currentAuthorizationUrl = Form.useWatch("authorization_url", form);
   const currentTokenUrl = Form.useWatch("token_url", form);
   const currentRegistrationUrl = Form.useWatch("registration_url", form);
+  const hasExistingToolAllowlist =
+    Boolean(mcpServer.mcp_info?.tool_allowlist_enforced) || (mcpServer.allowed_tools?.length ?? 0) > 0;
+  const existingAllowedTools = hasExistingToolAllowlist ? mcpServer.allowed_tools ?? [] : null;
 
   const persistEditUiState = () => {
     if (typeof window === "undefined") {
@@ -82,6 +86,7 @@
           formValues: values,
           costConfig,
           allowedTools,
+          hasToolAllowlistInteraction,
           searchValue,
           aliasManuallyEdited,
         }),
@@ -135,7 +140,7 @@
     },
     onTokenReceived: (token) => {
       setOauthAccessToken(token?.access_token ?? null);
-      
+
       if (token?.access_token) {
         const credentials = {
           access_token: token.access_token,
@@ -143,11 +148,11 @@
           ...(token.expires_in && { expires_in: token.expires_in }),
           ...(token.scope && { scope: token.scope }),
         };
-        
+
         form.setFieldsValue({ credentials });
-        
+
         NotificationsManager.success(
-          "OAuth authorization successful! Please click 'Update MCP Server' to save the credentials."
+          "OAuth authorization successful! Please click 'Update MCP Server' to save the credentials.",
         );
       }
     },
@@ -176,7 +181,6 @@
     }
   }, [mcpServer.env]);
 
-
   // If server has spec_path, show it as "openapi" transport in the UI
   const effectiveTransport = React.useMemo(() => {
     if (mcpServer.spec_path && mcpServer.transport !== "stdio") {
@@ -208,12 +212,16 @@
 
   // Initialize allowed tools and tool overrides from existing server data
   useEffect(() => {
-    if (mcpServer.allowed_tools) {
-      setAllowedTools(mcpServer.allowed_tools);
+    setHasToolAllowlistInteraction(false);
+  }, [mcpServer.server_id]);
+
+  useEffect(() => {
+    if (hasExistingToolAllowlist) {
+      setAllowedTools(mcpServer.allowed_tools ?? []);
     }
     setToolNameToDisplayName(mcpServer.tool_name_to_display_name ?? {});
     setToolNameToDescription(mcpServer.tool_name_to_description ?? {});
-  }, [mcpServer]);
+  }, [mcpServer, hasExistingToolAllowlist]);
 
   useEffect(() => {
     if (typeof window === "undefined") {
@@ -238,6 +246,9 @@
       if (parsed.allowedTools) {
         setAllowedTools(parsed.allowedTools);
       }
+      if (typeof parsed.hasToolAllowlistInteraction === "boolean") {
+        setHasToolAllowlistInteraction(parsed.hasToolAllowlistInteraction);
+      }
       if (parsed.searchValue) {
         setSearchValue(parsed.searchValue);
       }
@@ -529,6 +540,8 @@
         mcpServer.alias ||
         "unknown";
 
+      const toolAllowlistEnforced = hasExistingToolAllowlist || hasToolAllowlistInteraction || allowedTools.length > 0;
+
       const payload: Record<string, any> = {
         ...restValues,
         ...stdioFields,
@@ -537,16 +550,22 @@
         env_json: undefined,
         server_id: mcpServer.server_id,
         mcp_info: {
+          ...(mcpServer.mcp_info ?? {}),
           server_name: mcpInfoServerName,
           description: restValues.description,
           logo_url: logoUrl || undefined,
           mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
+          tool_allowlist_enforced: toolAllowlistEnforced,
         },
         mcp_access_groups: accessGroups,
         alias: restValues.alias,
         // Include permission management fields
         extra_headers: restValues.extra_headers || [],
-        allowed_tools: allowedTools.length > 0 ? allowedTools : null,
+        ...(toolAllowlistEnforced
+          ? {
+              allowed_tools: allowedTools,
+            }
+          : {}),
         tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null,
         tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null,
         disallowed_tools: restValues.disallowed_tools || [],
@@ -563,12 +582,11 @@
             ? Boolean(delegateAuthToUpstreamRaw ?? mcpServer.delegate_auth_to_upstream)
             : false,
         // Include token_validation when it is set (non-null) or when clearing an existing value
-        ...(tokenValidation !== null || mcpServer.token_validation
-          ? { token_validation: tokenValidation }
-          : {}),
+        ...(tokenValidation !== null || mcpServer.token_validation ? { token_validation: tokenValidation } : {}),
       };
 
-      const includeCredentials = restValues.auth_type && AUTH_TYPES_REQUIRING_CREDENTIALS.includes(restValues.auth_type);
+      const includeCredentials =
+        restValues.auth_type && AUTH_TYPES_REQUIRING_CREDENTIALS.includes(restValues.auth_type);
 
       if (includeCredentials && credentialsPayload && Object.keys(credentialsPayload).length > 0) {
         payload.credentials = credentialsPayload;
@@ -700,10 +718,7 @@
                   />
                 </Form.Item>
 
-                <Form.Item
-                  label="Args"
-                  name="args"
-                >
+                <Form.Item label="Args" name="args">
                   <Select
                     mode="tags"
                     size="large"
@@ -916,17 +931,15 @@
                       }
                       name="token_storage_ttl_seconds"
                     >
-                      <InputNumber
-                        min={1}
-                        placeholder="e.g. 3600"
-                        style={{ width: "100%" }}
-                        className="rounded-lg"
-                      />
+                      <InputNumber min={1} placeholder="e.g. 3600" style={{ width: "100%" }} className="rounded-lg" />
                     </Form.Item>
                   </>
                 )}
                 <div className="rounded-lg border border-dashed border-gray-300 p-4 space-y-2">
-                  <p className="text-sm text-gray-600">Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value.</p>
+                  <p className="text-sm text-gray-600">
+                    Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication
+                    value.
+                  </p>
                   <Button
                     variant="secondary"
                     onClick={startOAuthFlow}
@@ -952,7 +965,12 @@
               <>
                 <p className="text-sm text-gray-500 mb-2">
                   For MCP servers hosted on AWS Bedrock AgentCore.{" "}
-                  <a href="https://docs.litellm.ai/docs/mcp_aws_sigv4" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-700">
+                  <a
+                    href="https://docs.litellm.ai/docs/mcp_aws_sigv4"
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    className="text-blue-500 hover:text-blue-700"
+                  >
                     View docs &rarr;
                   </a>
                 </p>
@@ -1098,7 +1116,7 @@
                   transport: transportType ?? mcpServer.transport,
                   auth_type: currentAuthType ?? mcpServer.auth_type,
                   mcp_info: mcpServer.mcp_info,
-                  oauth_flow_type: (currentTokenUrl ?? mcpServer.token_url) ? OAUTH_FLOW.M2M : OAUTH_FLOW.INTERACTIVE,
+                  oauth_flow_type: currentTokenUrl ?? mcpServer.token_url ? OAUTH_FLOW.M2M : OAUTH_FLOW.INTERACTIVE,
                   static_headers: currentStaticHeaders ?? mcpServer.static_headers,
                   credentials: currentCredentials,
                   authorization_url: currentAuthorizationUrl ?? mcpServer.authorization_url,
@@ -1106,8 +1124,11 @@
                   registration_url: currentRegistrationUrl ?? mcpServer.registration_url,
                 }}
                 allowedTools={allowedTools}
-                existingAllowedTools={mcpServer.allowed_tools || null}
+                existingAllowedTools={existingAllowedTools}
+                hasToolAllowlistInteraction={hasToolAllowlistInteraction}
+                isEditMode
                 onAllowedToolsChange={setAllowedTools}
+                onToolAllowlistInteraction={() => setHasToolAllowlistInteraction(true)}
                 toolNameToDisplayName={toolNameToDisplayName}
                 toolNameToDescription={toolNameToDescription}
                 onToolNameToDisplayNameChange={setToolNameToDisplayName}

diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.test.tsx
new file mode 100644
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.test.tsx
@@ -1,0 +1,84 @@
+import { useState } from "react";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import MCPToolConfiguration from "./mcp_tool_configuration";
+
+const tools = [
+  { name: "read_user", description: "Read user" },
+  { name: "delete_user", description: "Delete user" },
+];
+
+const renderToolConfiguration = (onAllowedToolsChange = vi.fn()) => {
+  render(
+    <MCPToolConfiguration
+      accessToken="token"
+      formValues={{ url: "https://example.com/mcp", transport: "http", auth_type: "none" }}
+      allowedTools={[]}
+      existingAllowedTools={null}
+      onAllowedToolsChange={onAllowedToolsChange}
+      toolNameToDisplayName={{}}
+      toolNameToDescription={{}}
+      onToolNameToDisplayNameChange={vi.fn()}
+      onToolNameToDescriptionChange={vi.fn()}
+      externalTools={tools}
+      externalCanFetch
+      isEditMode
+    />,
+  );
+
+  return onAllowedToolsChange;
+};
+
+describe("MCPToolConfiguration", () => {
+  it("shows legacy unrestricted edit tools enabled in flat view", async () => {
+    const onAllowedToolsChange = renderToolConfiguration();
+
+    fireEvent.click(screen.getByText("Flat List"));
+
+    expect(screen.getByText("2 of 2 tools enabled for user access")).toBeInTheDocument();
+    expect(screen.getAllByText("Enabled")).toHaveLength(2);
+
+    fireEvent.click(screen.getByText("read_user"));
+
+    await waitFor(() => {
+      expect(onAllowedToolsChange).toHaveBeenLastCalledWith(["delete_user"]);
+    });
+  });
+
+  it("keeps legacy unrestricted tools disabled after all are toggled off", async () => {
+    const Wrapper = () => {
+      const [allowedTools, setAllowedTools] = useState<string[]>([]);
+      const [hasToolAllowlistInteraction, setHasToolAllowlistInteraction] = useState(false);
... diff truncated: showing 800 of 1285 lines

You can send follow-ups to the cloud agent here.

Comment thread ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
@Sameerlite

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 6290c4b. Configure here.

description: restValues.description,
logo_url: logoUrl || undefined,
mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
tool_allowlist_enforced: allowedTools.length > 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create flow omits enforcement flag when all tools deselected

Medium Severity

In the create flow, tool_allowlist_enforced is set to allowedTools.length > 0, which evaluates to false when a user explicitly deselects all tools. The backend's server_applies_tool_allowlist() then returns False (no enforcement, empty list), making all tools accessible despite the UI showing "0 tools enabled." The edit flow correctly handles this case using hasToolAllowlistInteraction tracking, but the create flow lacks this mechanism, creating a mismatch between displayed state and actual server behavior.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6290c4b. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

…d_tools

Pulls in the responses-API test fix (gemini-3-pro-preview -> gemini-3.1-pro-preview)
that resolves the llm_responses_api_testing failure caused by Google sunsetting
the deprecated model.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
Track explicit allowlist interaction in the create form so deselecting
every tool persists tool_allowlist_enforced=true. Previously an empty
selection sent the flag as false with allowed_tools=[], which the proxy
treats as allow-all, contradicting the UI's 0 tools enabled state. This
mirrors the existing edit-flow handling.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; thanks!

@mateo-berri
mateo-berri merged commit b7bbddb into litellm_internal_staging Jun 2, 2026
119 checks passed
@mateo-berri
mateo-berri deleted the litellm_mcp_clear_allowed_tools branch June 2, 2026 04:28
yugborana pushed a commit to yugborana/litellm that referenced this pull request Jun 2, 2026
…erriAI#29411)

* fix(mcp): clear allowed_tools and tool overrides on MCP server edit

Send empty arrays/objects from the dashboard instead of null, coerce legacy
null payloads before Prisma, and stop auto-selecting all tools when the
stored allowlist is empty.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(mcp): simplify CRUD panel value ternary per review

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): enforce empty tool allowlist when cleared in dashboard

Set mcp_info.tool_allowlist_enforced on UI save so [] blocks all tools
while legacy servers with default [] remain unrestricted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix legacy MCP tool allowlist edit state

* test(mcp): pin allowlist fields on mock server in tools test

MagicMock auto-attributes are truthy and trigger server_applies_tool_allowlist
after the empty-allowlist enforcement change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): avoid locking legacy servers on quick edit save

Only set tool_allowlist_enforced when already enforced or the user
selected tools; skip allowlist fields on save for unrestricted servers;
do not auto-select all tools when editing legacy servers before load.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): type mcp_info base for allowlist flag read

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): use MCPInfo type for tool_allowlist_enforced in edit save

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* Remove unused MCP allowlist variable

* Fix MCP legacy tool state display

* Fix legacy MCP tool allowlist saves

* fix(mcp): enforce allowlist when create flow deselects all tools

Track explicit allowlist interaction in the create form so deselecting
every tool persists tool_allowlist_enforced=true. Previously an empty
selection sent the flag as false with allowed_tools=[], which the proxy
treats as allow-all, contradicting the UI's 0 tools enabled state. This
mirrors the existing edit-flow handling.

* fix(mcp): enforce disallowed_tools on REST listing and keep restored tool selection on legacy edit

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
tin-berri pushed a commit that referenced this pull request Jun 10, 2026
…29411)

* fix(mcp): clear allowed_tools and tool overrides on MCP server edit

Send empty arrays/objects from the dashboard instead of null, coerce legacy
null payloads before Prisma, and stop auto-selecting all tools when the
stored allowlist is empty.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(mcp): simplify CRUD panel value ternary per review

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): enforce empty tool allowlist when cleared in dashboard

Set mcp_info.tool_allowlist_enforced on UI save so [] blocks all tools
while legacy servers with default [] remain unrestricted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix legacy MCP tool allowlist edit state

* test(mcp): pin allowlist fields on mock server in tools test

MagicMock auto-attributes are truthy and trigger server_applies_tool_allowlist
after the empty-allowlist enforcement change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): avoid locking legacy servers on quick edit save

Only set tool_allowlist_enforced when already enforced or the user
selected tools; skip allowlist fields on save for unrestricted servers;
do not auto-select all tools when editing legacy servers before load.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): type mcp_info base for allowlist flag read

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): use MCPInfo type for tool_allowlist_enforced in edit save

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* Remove unused MCP allowlist variable

* Fix MCP legacy tool state display

* Fix legacy MCP tool allowlist saves

* fix(mcp): enforce allowlist when create flow deselects all tools

Track explicit allowlist interaction in the create form so deselecting
every tool persists tool_allowlist_enforced=true. Previously an empty
selection sent the flag as false with allowed_tools=[], which the proxy
treats as allow-all, contradicting the UI's 0 tools enabled state. This
mirrors the existing edit-flow handling.

* fix(mcp): enforce disallowed_tools on REST listing and keep restored tool selection on legacy edit

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
tin-berri pushed a commit that referenced this pull request Jun 10, 2026
…29411)

* fix(mcp): clear allowed_tools and tool overrides on MCP server edit

Send empty arrays/objects from the dashboard instead of null, coerce legacy
null payloads before Prisma, and stop auto-selecting all tools when the
stored allowlist is empty.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(mcp): simplify CRUD panel value ternary per review

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): enforce empty tool allowlist when cleared in dashboard

Set mcp_info.tool_allowlist_enforced on UI save so [] blocks all tools
while legacy servers with default [] remain unrestricted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix legacy MCP tool allowlist edit state

* test(mcp): pin allowlist fields on mock server in tools test

MagicMock auto-attributes are truthy and trigger server_applies_tool_allowlist
after the empty-allowlist enforcement change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): avoid locking legacy servers on quick edit save

Only set tool_allowlist_enforced when already enforced or the user
selected tools; skip allowlist fields on save for unrestricted servers;
do not auto-select all tools when editing legacy servers before load.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): type mcp_info base for allowlist flag read

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): use MCPInfo type for tool_allowlist_enforced in edit save

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* Remove unused MCP allowlist variable

* Fix MCP legacy tool state display

* Fix legacy MCP tool allowlist saves

* fix(mcp): enforce allowlist when create flow deselects all tools

Track explicit allowlist interaction in the create form so deselecting
every tool persists tool_allowlist_enforced=true. Previously an empty
selection sent the flag as false with allowed_tools=[], which the proxy
treats as allow-all, contradicting the UI's 0 tools enabled state. This
mirrors the existing edit-flow handling.

* fix(mcp): enforce disallowed_tools on REST listing and keep restored tool selection on legacy edit

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
hbjydev pushed a commit to hbjydev/phoebe that referenced this pull request Jun 14, 2026
…9.0) (#93)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | minor | `v1.88.1` → `v1.89.0` |

---

### Release Notes

<details>
<summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary>

### [`v1.89.0`](https://github.com/BerriAI/litellm/releases/tag/v1.89.0)

[Compare Source](https://github.com/BerriAI/litellm/compare/v1.89.0...v1.89.0)

##### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.89.0
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.89.0/cosign.pub \
  ghcr.io/berriai/litellm:v1.89.0
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

##### What's Changed

- test(responses): bump deprecated gemini-3-pro-preview to gemini-3.1-pro-preview by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29433](https://github.com/BerriAI/litellm/pull/29433)
- fix: map mistral/ministral-8b-latest in model price map by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29453](https://github.com/BerriAI/litellm/pull/29453)
- fix(datadog): split oversized batches on 413 instead of re-queueing forever by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29444](https://github.com/BerriAI/litellm/pull/29444)
- feat(otel): allowlist team\_metadata sub-keys promoted to baggage by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29442](https://github.com/BerriAI/litellm/pull/29442)
- fix: stop use\_chat\_completions\_api flag from leaking into provider request body by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29447](https://github.com/BerriAI/litellm/pull/29447)
- fix(anthropic, fireworks): inline legacy $ref defs in tool schemas by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28646](https://github.com/BerriAI/litellm/pull/28646)
- fix(proxy): omit OpenAI \[DONE] on google-genai streamGenerateContent by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29426](https://github.com/BerriAI/litellm/pull/29426)
- ci(release): create stable/X.Y.x line branch on X.Y.0 tags by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29457](https://github.com/BerriAI/litellm/pull/29457)
- fix(vector-stores): support engines URL for Vertex AI Search by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;27885](https://github.com/BerriAI/litellm/pull/27885)
- fix(ui): render caller-supplied filter options in caller order by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29462](https://github.com/BerriAI/litellm/pull/29462)
- fix(batches): skip unnecessary batch input file reads by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29114](https://github.com/BerriAI/litellm/pull/29114)
- docs(agents): clarify when to create new test files by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29472](https://github.com/BerriAI/litellm/pull/29472)
- Litellm OSS Staging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29161](https://github.com/BerriAI/litellm/pull/29161)
- fix(mcp): clear allowed\_tools and tool overrides on MCP server edit by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29411](https://github.com/BerriAI/litellm/pull/29411)
- Litellm OSS Staging 010626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29422](https://github.com/BerriAI/litellm/pull/29422)
- fix(ci): make CircleCI rerun-failed-tests collect tests when 2+ test files fail by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29475](https://github.com/BerriAI/litellm/pull/29475)
- feat(a2a): watsonx Orchestrate agent provider by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29410](https://github.com/BerriAI/litellm/pull/29410)
- fix(azure\_ai): strip tool-level extra fields on 400 and retry by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29479](https://github.com/BerriAI/litellm/pull/29479)
- fix(docs): remove fixed dimensions from README hero image by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29496](https://github.com/BerriAI/litellm/pull/29496)
- Litellm oss staging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29492](https://github.com/BerriAI/litellm/pull/29492)
- fix: small CLAUDE.md nits by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29504](https://github.com/BerriAI/litellm/pull/29504)
- Add MCP semantic conventions to otelv2 by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29468](https://github.com/BerriAI/litellm/pull/29468)
- fix(passthrough): emit otel guardrail span when a guardrail blocks by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29470](https://github.com/BerriAI/litellm/pull/29470)
- fix(proxy): strip NUL bytes from spend log payloads to prevent PostgreSQL 22P05 by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29515](https://github.com/BerriAI/litellm/pull/29515)
- \[internal copy of [#&#8203;28008](https://github.com/BerriAI/litellm/issues/28008)] Support MCP OAuth passthrough and issuer-scoped JWT auth by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28356](https://github.com/BerriAI/litellm/pull/28356)
- feat(vector-stores): forward per-request params to Vertex AI Search by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29459](https://github.com/BerriAI/litellm/pull/29459)
- feat(proxy): add per-MCP-server RPM rate limiting for keys and teams by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29482](https://github.com/BerriAI/litellm/pull/29482)
- fix(tests): drop module-level test calls that break local\_testing collection by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29520](https://github.com/BerriAI/litellm/pull/29520)
- feat(agents): add LangFlow agent provider with A2A session bridging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28963](https://github.com/BerriAI/litellm/pull/28963)
- fix(ui/agents): make A2A skill tags enterable and validated by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29512](https://github.com/BerriAI/litellm/pull/29512)
- \[internal copy of [#&#8203;29232](https://github.com/BerriAI/litellm/issues/29232)] feat: route future Claude models to Anthropic provider via pattern matching by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29239](https://github.com/BerriAI/litellm/pull/29239)
- fix(tests): drop import-time completion call in test\_register\_model by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29521](https://github.com/BerriAI/litellm/pull/29521)
- test: stabilize batch VCR coverage and stop live upload/network leaks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29477](https://github.com/BerriAI/litellm/pull/29477)
- \[internal copy of [#&#8203;29003](https://github.com/BerriAI/litellm/issues/29003)] fix(vertex\_ai): use user-supplied api\_base as is for Model Garden OpenAI-compat path by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29530](https://github.com/BerriAI/litellm/pull/29530)
- feat(proxy): native /health/drain preStop hook for graceful shutdown by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29439](https://github.com/BerriAI/litellm/pull/29439)
- fix(auth): preserve 401 status for expired JWTs in OTel traces by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29510](https://github.com/BerriAI/litellm/pull/29510)
- fix(otel): capture 401 error details in management endpoint spans by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29535](https://github.com/BerriAI/litellm/pull/29535)
- test(proxy/utils): pin bottom-of-file helper behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29509](https://github.com/BerriAI/litellm/pull/29509)
- test(proxy/utils): pin PrismaClient and spend-update behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29488](https://github.com/BerriAI/litellm/pull/29488)
- test(proxy/utils): pin ProxyLogging behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29485](https://github.com/BerriAI/litellm/pull/29485)
- fix: missing span for guardrail passthrough by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29552](https://github.com/BerriAI/litellm/pull/29552)
- fix(auth): let internal users view search tools by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29542](https://github.com/BerriAI/litellm/pull/29542)
- fix: missing mcp otel attributes by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29554](https://github.com/BerriAI/litellm/pull/29554)
- fix(proxy): resolve managed video model ids for auth by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29545](https://github.com/BerriAI/litellm/pull/29545)
- fix(key\_generate): allow team members to create keys on org-scoped teams by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29310](https://github.com/BerriAI/litellm/pull/29310)
- test(pass-through): move Gemini pass-through tests to gemini-3.1-flash-lite by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29595](https://github.com/BerriAI/litellm/pull/29595)
- Litellm oss staging 030626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29578](https://github.com/BerriAI/litellm/pull/29578)
- Fix : a2a bugs 030626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29566](https://github.com/BerriAI/litellm/pull/29566)
- \[internal copy of [#&#8203;29533](https://github.com/BerriAI/litellm/issues/29533)] fix(anthropic/adapter): emit thinking block for reasoning\_content-only streaming chunks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29600](https://github.com/BerriAI/litellm/pull/29600)
- ci: reproduce default-Windows wheel install to guard MAX\_PATH by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29597](https://github.com/BerriAI/litellm/pull/29597)
- fix(vertex): strip output\_config.effort for Vertex Claude models that reject it (Haiku 4.5) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29585](https://github.com/BerriAI/litellm/pull/29585)
- Litellm websocket improvements by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29563](https://github.com/BerriAI/litellm/pull/29563)
- feat(arize/phoenix): OpenInference rendering parity — tool\_calls, cost, passthrough I/O, session/user, multimodal, cache tokens by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28800](https://github.com/BerriAI/litellm/pull/28800)
- \[internal copy of [#&#8203;29550](https://github.com/BerriAI/litellm/issues/29550)] fix: passthrough endpoints duplicate logs by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29598](https://github.com/BerriAI/litellm/pull/29598)
- fix(ci): keep coverage rename green when a parallel node runs no tests by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29608](https://github.com/BerriAI/litellm/pull/29608)
- test(vcr): close out the remaining VCR live-call leaks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29603](https://github.com/BerriAI/litellm/pull/29603)
- fix(key\_generate): exempt UI/CLI session tokens from the budget ceiling for team keys by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29612](https://github.com/BerriAI/litellm/pull/29612)
- fix(realtime): allow null transcripts in stream logging payloads by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29625](https://github.com/BerriAI/litellm/pull/29625)
- build(ui): migrate eslint to flat config + bump eslint-config-next to 16 by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29626](https://github.com/BerriAI/litellm/pull/29626)
- fix(key\_generate): scope session-token team-key budget exemption to caller-supplied team\_id by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29641](https://github.com/BerriAI/litellm/pull/29641)
- fix(proxy): disable proxy buffering on streaming SSE responses by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29557](https://github.com/BerriAI/litellm/pull/29557)
- fix(mcp): gate /public/mcp\_hub strictly on litellm.public\_mcp\_servers by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;27764](https://github.com/BerriAI/litellm/pull/27764)
- ci(ui): frontend-lint job enforcing prettier + eslint on changed files by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29633](https://github.com/BerriAI/litellm/pull/29633)
- fix(gemini): googleSearch + server-side tools and googleMaps JSON schema by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29582](https://github.com/BerriAI/litellm/pull/29582)
- fix(proxy): passthrough 404 when SERVER\_ROOT\_PATH is set by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29658](https://github.com/BerriAI/litellm/pull/29658)
- fix(gemini-realtime): use GA event names for Pipecat 1.3.x compatibility by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29662](https://github.com/BerriAI/litellm/pull/29662)
- Litellm oss staging 040626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29671](https://github.com/BerriAI/litellm/pull/29671)
- style(ui): prettier formatting pass over the dashboard by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29622](https://github.com/BerriAI/litellm/pull/29622)
- chore: ignore prettier dashboard reformat in git blame by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29695](https://github.com/BerriAI/litellm/pull/29695)
- fix(helm): Enable Backend Deployment to mount Gateway config.yaml by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29605](https://github.com/BerriAI/litellm/pull/29605)
- \[internal copy of [#&#8203;29277](https://github.com/BerriAI/litellm/issues/29277)] fix(proxy): add default=None to LiteLLM\_TeamMembership.litellm\_budget\_table by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29684](https://github.com/BerriAI/litellm/pull/29684)
- test: make custom\_tokenizer proxy tests hermetic by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29643](https://github.com/BerriAI/litellm/pull/29643)
- test(proxy): stop running real-DB tests in GitHub Actions unit jobs by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29700](https://github.com/BerriAI/litellm/pull/29700)
- chore(ui): remove the bare-fetch lint rule by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29712](https://github.com/BerriAI/litellm/pull/29712)
- Litellm jwt mapping virtualkeys by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;28510](https://github.com/BerriAI/litellm/pull/28510)
- refactor(ui): shared HTTP client + location-pinned fetch() lint rule by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29723](https://github.com/BerriAI/litellm/pull/29723)
- fix(proxy): stop team BYOK model name corruption on model edit by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29731](https://github.com/BerriAI/litellm/pull/29731)
- \[internal copy of [#&#8203;29511](https://github.com/BerriAI/litellm/issues/29511)] feat(guardrails): add sensitive data routing to on-premise models by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29531](https://github.com/BerriAI/litellm/pull/29531)
- fix(proxy/hooks): populate llm\_provider on internal rate-limit errors by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;27707](https://github.com/BerriAI/litellm/pull/27707)
- fix(vertex/anthropic): handle namespace tools and strip client\_metadata for codex compatibility by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29489](https://github.com/BerriAI/litellm/pull/29489)
- Support OAuth M2M for Databricks Apps A2A agents by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29586](https://github.com/BerriAI/litellm/pull/29586)
- fix: small CLAUDE.md nit by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29749](https://github.com/BerriAI/litellm/pull/29749)
- fix(anthropic): route Claude Opus 4.8 through adaptive thinking by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29702](https://github.com/BerriAI/litellm/pull/29702)
- fix(proxy): persist oauth2\_flow on MCP server registration by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;29690](https://github.com/BerriAI/litellm/pull/29690)
- \[internal copy of [#&#8203;27491](https://github.com/BerriAI/litellm/issues/27491)] fix(realtime): Fix Realtime Audio Token Cost Tracking by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29722](https://github.com/BerriAI/litellm/pull/29722)
- fix(galileo): use ingest traces API and standard logging payload by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29651](https://github.com/BerriAI/litellm/pull/29651)
- fix(auth): expand all-team-models sentinel in can\_key\_call\_model for batch validation by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29746](https://github.com/BerriAI/litellm/pull/29746)
- test(vcr): stop refreshing cassette TTL on read so cassettes lapse after 24h by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29784](https://github.com/BerriAI/litellm/pull/29784)
- test(ci): record/replay OpenAI image gen so the spend E2E isn't outage-bound by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29787](https://github.com/BerriAI/litellm/pull/29787)
- fix(ui): route MCP playground auth by oauth2 mode instead of token\_url by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29714](https://github.com/BerriAI/litellm/pull/29714)
- refactor(ui): centralize proxy base URL resolution into tested resolver by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29793](https://github.com/BerriAI/litellm/pull/29793)
- Litellm oss staging 050626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29774](https://github.com/BerriAI/litellm/pull/29774)
- test(google): add google-genai SDK proxy integration tests by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29781](https://github.com/BerriAI/litellm/pull/29781)
- fix(jwt): use resolved DB user\_id for spend on legacy email match by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29217](https://github.com/BerriAI/litellm/pull/29217)
- feat(ui): generate dashboard API types from the proxy OpenAPI spec by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29816](https://github.com/BerriAI/litellm/pull/29816)
- fix(proxy): drop deleted team BYOK model name from team.models by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29820](https://github.com/BerriAI/litellm/pull/29820)
- feat(mcp): per-server env vars with global + per-user scopes by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28917](https://github.com/BerriAI/litellm/pull/28917)
- refactor(ui): route behavior-preserving networking calls through apiClient by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29806](https://github.com/BerriAI/litellm/pull/29806)
- fix(mcp): persist Tools-tab MCP OAuth token to DB by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29809](https://github.com/BerriAI/litellm/pull/29809)
- fix(ui): require new expiration when regenerating an expired key by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29838](https://github.com/BerriAI/litellm/pull/29838)
- refactor(ui): route query-building networking calls through apiClient by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29815](https://github.com/BerriAI/litellm/pull/29815)
- Make the image-gen record/replay proxy report cache mode and per-request HIT/MISS by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29802](https://github.com/BerriAI/litellm/pull/29802)
- feat(proxy): hot-reload .env in dev when running with --reload by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29783](https://github.com/BerriAI/litellm/pull/29783)
- fix(ui): stop MCP playground tool calls from sending twice by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29821](https://github.com/BerriAI/litellm/pull/29821)
- feat(fal\_ai): add Nano Banana / Gemini 2.5 Flash Image generation support by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29798](https://github.com/BerriAI/litellm/pull/29798)
- Title: Fix managed batch cancel credential resolution by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29734](https://github.com/BerriAI/litellm/pull/29734)
- Title: fix(proxy): resolve vector store file list credentials from team deployments by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29739](https://github.com/BerriAI/litellm/pull/29739)
- refactor: convert AWS and GCP Terraform stacks into reusable modules … by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28103](https://github.com/BerriAI/litellm/pull/28103)
- chore(ui): build ui for release by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29853](https://github.com/BerriAI/litellm/pull/29853)
- fix(terraform/gcp): prompt for image\_registry in DeployStack one-click by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29852](https://github.com/BerriAI/litellm/pull/29852)
- fix(terraform/gcp): abandon SQL user on destroy by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29855](https://github.com/BerriAI/litellm/pull/29855)
- Extend the record/replay proxy to chat, embeddings, moderations, rerank, and Anthropic by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29847](https://github.com/BerriAI/litellm/pull/29847)
- chore(deps): bump deps by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29860](https://github.com/BerriAI/litellm/pull/29860)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29861](https://github.com/BerriAI/litellm/pull/29861)
- fix: 400 on Anthropic context overflow; seed identity on failed auth by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29848](https://github.com/BerriAI/litellm/pull/29848)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29862](https://github.com/BerriAI/litellm/pull/29862)
- chore(release): patch v1.89.0-rc.1 with [#&#8203;30064](https://github.com/BerriAI/litellm/issues/30064) (Claude Fable 5) for v1.89.0-rc.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;30143](https://github.com/BerriAI/litellm/pull/30143)

**Full Changelog**: <https://github.com/BerriAI/litellm/compare/v1.88.0...v1.89.0>

### [`v1.89.0`](https://github.com/BerriAI/litellm/releases/tag/v1.89.0)

[Compare Source](https://github.com/BerriAI/litellm/compare/v1.88.2...v1.89.0)

##### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.89.0
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.89.0/cosign.pub \
  ghcr.io/berriai/litellm:v1.89.0
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

##### What's Changed

- test(responses): bump deprecated gemini-3-pro-preview to gemini-3.1-pro-preview by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29433](https://github.com/BerriAI/litellm/pull/29433)
- fix: map mistral/ministral-8b-latest in model price map by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29453](https://github.com/BerriAI/litellm/pull/29453)
- fix(datadog): split oversized batches on 413 instead of re-queueing forever by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29444](https://github.com/BerriAI/litellm/pull/29444)
- feat(otel): allowlist team\_metadata sub-keys promoted to baggage by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29442](https://github.com/BerriAI/litellm/pull/29442)
- fix: stop use\_chat\_completions\_api flag from leaking into provider request body by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29447](https://github.com/BerriAI/litellm/pull/29447)
- fix(anthropic, fireworks): inline legacy $ref defs in tool schemas by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28646](https://github.com/BerriAI/litellm/pull/28646)
- fix(proxy): omit OpenAI \[DONE] on google-genai streamGenerateContent by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29426](https://github.com/BerriAI/litellm/pull/29426)
- ci(release): create stable/X.Y.x line branch on X.Y.0 tags by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29457](https://github.com/BerriAI/litellm/pull/29457)
- fix(vector-stores): support engines URL for Vertex AI Search by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;27885](https://github.com/BerriAI/litellm/pull/27885)
- fix(ui): render caller-supplied filter options in caller order by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29462](https://github.com/BerriAI/litellm/pull/29462)
- fix(batches): skip unnecessary batch input file reads by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29114](https://github.com/BerriAI/litellm/pull/29114)
- docs(agents): clarify when to create new test files by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29472](https://github.com/BerriAI/litellm/pull/29472)
- Litellm OSS Staging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29161](https://github.com/BerriAI/litellm/pull/29161)
- fix(mcp): clear allowed\_tools and tool overrides on MCP server edit by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29411](https://github.com/BerriAI/litellm/pull/29411)
- Litellm OSS Staging 010626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29422](https://github.com/BerriAI/litellm/pull/29422)
- fix(ci): make CircleCI rerun-failed-tests collect tests when 2+ test files fail by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29475](https://github.com/BerriAI/litellm/pull/29475)
- feat(a2a): watsonx Orchestrate agent provider by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29410](https://github.com/BerriAI/litellm/pull/29410)
- fix(azure\_ai): strip tool-level extra fields on 400 and retry by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29479](https://github.com/BerriAI/litellm/pull/29479)
- fix(docs): remove fixed dimensions from README hero image by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29496](https://github.com/BerriAI/litellm/pull/29496)
- Litellm oss staging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29492](https://github.com/BerriAI/litellm/pull/29492)
- fix: small CLAUDE.md nits by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29504](https://github.com/BerriAI/litellm/pull/29504)
- Add MCP semantic conventions to otelv2 by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29468](https://github.com/BerriAI/litellm/pull/29468)
- fix(passthrough): emit otel guardrail span when a guardrail blocks by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29470](https://github.com/BerriAI/litellm/pull/29470)
- fix(proxy): strip NUL bytes from spend log payloads to prevent PostgreSQL 22P05 by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29515](https://github.com/BerriAI/litellm/pull/29515)
- \[internal copy of [#&#8203;28008](https://github.com/BerriAI/litellm/issues/28008)] Support MCP OAuth passthrough and issuer-scoped JWT auth by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28356](https://github.com/BerriAI/litellm/pull/28356)
- feat(vector-stores): forward per-request params to Vertex AI Search by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29459](https://github.com/BerriAI/litellm/pull/29459)
- feat(proxy): add per-MCP-server RPM rate limiting for keys and teams by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29482](https://github.com/BerriAI/litellm/pull/29482)
- fix(tests): drop module-level test calls that break local\_testing collection by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29520](https://github.com/BerriAI/litellm/pull/29520)
- feat(agents): add LangFlow agent provider with A2A session bridging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28963](https://github.com/BerriAI/litellm/pull/28963)
- fix(ui/agents): make A2A skill tags enterable and validated by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29512](https://github.com/BerriAI/litellm/pull/29512)
- \[internal copy of [#&#8203;29232](https://github.com/BerriAI/litellm/issues/29232)] feat: route future Claude models to Anthropic provider via pattern matching by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29239](https://github.com/BerriAI/litellm/pull/29239)
- fix(tests): drop import-time completion call in test\_register\_model by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29521](https://github.com/BerriAI/litellm/pull/29521)
- test: stabilize batch VCR coverage and stop live upload/network leaks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29477](https://github.com/BerriAI/litellm/pull/29477)
- \[internal copy of [#&#8203;29003](https://github.com/BerriAI/litellm/issues/29003)] fix(vertex\_ai): use user-supplied api\_base as is for Model Garden OpenAI-compat path by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29530](https://github.com/BerriAI/litellm/pull/29530)
- feat(proxy): native /health/drain preStop hook for graceful shutdown by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29439](https://github.com/BerriAI/litellm/pull/29439)
- fix(auth): preserve 401 status for expired JWTs in OTel traces by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29510](https://github.com/BerriAI/litellm/pull/29510)
- fix(otel): capture 401 error details in management endpoint spans by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29535](https://github.com/BerriAI/litellm/pull/29535)
- test(proxy/utils): pin bottom-of-file helper behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29509](https://github.com/BerriAI/litellm/pull/29509)
- test(proxy/utils): pin PrismaClient and spend-update behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29488](https://github.com/BerriAI/litellm/pull/29488)
- test(proxy/utils): pin ProxyLogging behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29485](https://github.com/BerriAI/litellm/pull/29485)
- fix: missing span for guardrail passthrough by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29552](https://github.com/BerriAI/litellm/pull/29552)
- fix(auth): let internal users view search tools by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29542](https://github.com/BerriAI/litellm/pull/29542)
- fix: missing mcp otel attributes by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29554](https://github.com/BerriAI/litellm/pull/29554)
- fix(proxy): resolve managed video model ids for auth by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29545](https://github.com/BerriAI/litellm/pull/29545)
- fix(key\_generate): allow team members to create keys on org-scoped teams by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29310](https://github.com/BerriAI/litellm/pull/29310)
- test(pass-through): move Gemini pass-through tests to gemini-3.1-flash-lite by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29595](https://github.com/BerriAI/litellm/pull/29595)
- Litellm oss staging 030626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29578](https://github.com/BerriAI/litellm/pull/29578)
- Fix : a2a bugs 030626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29566](https://github.com/BerriAI/litellm/pull/29566)
- \[internal copy of [#&#8203;29533](https://github.com/BerriAI/litellm/issues/29533)] fix(anthropic/adapter): emit thinking block for reasoning\_content-only streaming chunks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29600](https://github.com/BerriAI/litellm/pull/29600)
- ci: reproduce default-Windows wheel install to guard MAX\_PATH by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29597](https://github.com/BerriAI/litellm/pull/29597)
- fix(vertex): strip output\_config.effort for Vertex Claude models that reject it (Haiku 4.5) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29585](https://github.com/BerriAI/litellm/pull/29585)
- Litellm websocket improvements by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29563](https://github.com/BerriAI/litellm/pull/29563)
- feat(arize/phoenix): OpenInference rendering parity — tool\_calls, cost, passthrough I/O, session/user, multimodal, cache tokens by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28800](https://github.com/BerriAI/litellm/pull/28800)
- \[internal copy of [#&#8203;29550](https://github.com/BerriAI/litellm/issues/29550)] fix: passthrough endpoints duplicate logs by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29598](https://github.com/BerriAI/litellm/pull/29598)
- fix(ci): keep coverage rename green when a parallel node runs no tests by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29608](https://github.com/BerriAI/litellm/pull/29608)
- test(vcr): close out the remaining VCR live-call leaks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29603](https://github.com/BerriAI/litellm/pull/29603)
- fix(key\_generate): exempt UI/CLI session tokens from the budget ceiling for team keys by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29612](https://github.com/BerriAI/litellm/pull/29612)
- fix(realtime): allow null transcripts in stream logging payloads by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29625](https://github.com/BerriAI/litellm/pull/29625)
- build(ui): migrate eslint to flat config + bump eslint-config-next to 16 by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29626](https://github.com/BerriAI/litellm/pull/29626)
- fix(key\_generate): scope session-token team-key budget exemption to caller-supplied team\_id by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29641](https://github.com/BerriAI/litellm/pull/29641)
- fix(proxy): disable proxy buffering on streaming SSE responses by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29557](https://github.com/BerriAI/litellm/pull/29557)
- fix(mcp): gate /public/mcp\_hub strictly on litellm.public\_mcp\_servers by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;27764](https://github.com/BerriAI/litellm/pull/27764)
- ci(ui): frontend-lint job enforcing prettier + eslint on changed files by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29633](https://github.com/BerriAI/litellm/pull/29633)
- fix(gemini): googleSearch + server-side tools and googleMaps JSON schema by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29582](https://github.com/BerriAI/litellm/pull/29582)
- fix(proxy): passthrough 404 when SERVER\_ROOT\_PATH is set by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29658](https://github.com/BerriAI/litellm/pull/29658)
- fix(gemini-realtime): use GA event names for Pipecat 1.3.x compatibility by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29662](https://github.com/BerriAI/litellm/pull/29662)
- Litellm oss staging 040626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29671](https://github.com/BerriAI/litellm/pull/29671)
- style(ui): prettier formatting pass over the dashboard by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29622](https://github.com/BerriAI/litellm/pull/29622)
- chore: ignore prettier dashboard reformat in git blame by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29695](https://github.com/BerriAI/litellm/pull/29695)
- fix(helm): Enable Backend Deployment to mount Gateway config.yaml by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29605](https://github.com/BerriAI/litellm/pull/29605)
- \[internal copy of [#&#8203;29277](https://github.com/BerriAI/litellm/issues/29277)] fix(proxy): add default=None to LiteLLM\_TeamMembership.litellm\_budget\_table by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29684](https://github.com/BerriAI/litellm/pull/29684)
- test: make custom\_tokenizer proxy tests hermetic by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29643](https://github.com/BerriAI/litellm/pull/29643)
- test(proxy): stop running real-DB tests in GitHub Actions unit jobs by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29700](https://github.com/BerriAI/litellm/pull/29700)
- chore(ui): remove the bare-fetch lint rule by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29712](https://github.com/BerriAI/litellm/pull/29712)
- Litellm jwt mapping virtualkeys by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;28510](https://github.com/BerriAI/litellm/pull/28510)
- refactor(ui): shared HTTP client + location-pinned fetch() lint rule by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29723](https://github.com/BerriAI/litellm/pull/29723)
- fix(proxy): stop team BYOK model name corruption on model edit by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29731](https://github.com/BerriAI/litellm/pull/29731)
- \[internal copy of [#&#8203;29511](https://github.com/BerriAI/litellm/issues/29511)] feat(guardrails): add sensitive data routing to on-premise models by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29531](https://github.com/BerriAI/litellm/pull/29531)
- fix(proxy/hooks): populate llm\_provider on internal rate-limit errors by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;27707](https://github.com/BerriAI/litellm/pull/27707)
- fix(vertex/anthropic): handle namespace tools and strip client\_metadata for codex compatibility by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29489](https://github.com/BerriAI/litellm/pull/29489)
- Support OAuth M2M for Databricks Apps A2A agents by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29586](https://github.com/BerriAI/litellm/pull/29586)
- fix: small CLAUDE.md nit by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29749](https://github.com/BerriAI/litellm/pull/29749)
- fix(anthropic): route Claude Opus 4.8 through adaptive thinking by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29702](https://github.com/BerriAI/litellm/pull/29702)
- fix(proxy): persist oauth2\_flow on MCP server registration by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;29690](https://github.com/BerriAI/litellm/pull/29690)
- \[internal copy of [#&#8203;27491](https://github.com/BerriAI/litellm/issues/27491)] fix(realtime): Fix Realtime Audio Token Cost Tracking by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29722](https://github.com/BerriAI/litellm/pull/29722)
- fix(galileo): use ingest traces API and standard logging payload by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29651](https://github.com/BerriAI/litellm/pull/29651)
- fix(auth): expand all-team-models sentinel in can\_key\_call\_model for batch validation by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29746](https://github.com/BerriAI/litellm/pull/29746)
- test(vcr): stop refreshing cassette TTL on read so cassettes lapse after 24h by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29784](https://github.com/BerriAI/litellm/pull/29784)
- test(ci): record/replay OpenAI image gen so the spend E2E isn't outage-bound by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29787](https://github.com/BerriAI/litellm/pull/29787)
- fix(ui): route MCP playground auth by oauth2 mode instead of token\_url by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29714](https://github.com/BerriAI/litellm/pull/29714)
- refactor(ui): centralize proxy base URL resolution into tested resolver by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29793](https://github.com/BerriAI/litellm/pull/29793)
- Litellm oss staging 050626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29774](https://github.com/BerriAI/litellm/pull/29774)
- test(google): add google-genai SDK proxy integration tests by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29781](https://github.com/BerriAI/litellm/pull/29781)
- fix(jwt): use resolved DB user\_id for spend on legacy email match by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29217](https://github.com/BerriAI/litellm/pull/29217)
- feat(ui): generate dashboard API types from the proxy OpenAPI spec by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29816](https://github.com/BerriAI/litellm/pull/29816)
- fix(proxy): drop deleted team BYOK model name from team.models by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29820](https://github.com/BerriAI/litellm/pull/29820)
- feat(mcp): per-server env vars with global + per-user scopes by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28917](https://github.com/BerriAI/litellm/pull/28917)
- refactor(ui): route behavior-preserving networking calls through apiClient by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29806](https://github.com/BerriAI/litellm/pull/29806)
- fix(mcp): persist Tools-tab MCP OAuth token to DB by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29809](https://github.com/BerriAI/litellm/pull/29809)
- fix(ui): require new expiration when regenerating an expired key by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29838](https://github.com/BerriAI/litellm/pull/29838)
- refactor(ui): route query-building networking calls through apiClient by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29815](https://github.com/BerriAI/litellm/pull/29815)
- Make the image-gen record/replay proxy report cache mode and per-request HIT/MISS by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29802](https://github.com/BerriAI/litellm/pull/29802)
- feat(proxy): hot-reload .env in dev when running with --reload by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29783](https://github.com/BerriAI/litellm/pull/29783)
- fix(ui): stop MCP playground tool calls from sending twice by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29821](https://github.com/BerriAI/litellm/pull/29821)
- feat(fal\_ai): add Nano Banana / Gemini 2.5 Flash Image generation support by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29798](https://github.com/BerriAI/litellm/pull/29798)
- Title: Fix managed batch cancel credential resolution by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29734](https://github.com/BerriAI/litellm/pull/29734)
- Title: fix(proxy): resolve vector store file list credentials from team deployments by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29739](https://github.com/BerriAI/litellm/pull/29739)
- refactor: convert AWS and GCP Terraform stacks into reusable modules … by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28103](https://github.com/BerriAI/litellm/pull/28103)
- chore(ui): build ui for release by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29853](https://github.com/BerriAI/litellm/pull/29853)
- fix(terraform/gcp): prompt for image\_registry in DeployStack one-click by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29852](https://github.com/BerriAI/litellm/pull/29852)
- fix(terraform/gcp): abandon SQL user on destroy by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29855](https://github.com/BerriAI/litellm/pull/29855)
- Extend the record/replay proxy to chat, embeddings, moderations, rerank, and Anthropic by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29847](https://github.com/BerriAI/litellm/pull/29847)
- chore(deps): bump deps by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29860](https://github.com/BerriAI/litellm/pull/29860)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29861](https://github.com/BerriAI/litellm/pull/29861)
- fix: 400 on Anthropic context overflow; seed identity on failed auth by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29848](https://github.com/BerriAI/litellm/pull/29848)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29862](https://github.com/BerriAI/litellm/pull/29862)
- chore(release): patch v1.89.0-rc.1 with [#&#8203;30064](https://github.com/BerriAI/litellm/issues/30064) (Claude Fable 5) for v1.89.0-rc.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;30143](https://github.com/BerriAI/litellm/pull/30143)

**Full Changelog**: <https://github.com/BerriAI/litellm/compare/v1.88.0...v1.89.0>

### [`v1.88.2`](https://github.com/BerriAI/litellm/releases/tag/v1.88.2)

[Compare Source](https://github.com/BerriAI/litellm/compare/v1.88.2...v1.88.2)

##### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.88.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

##### What's Changed

- chore(release): backport Fable 5, batch-file auth, CrowdStrike AIDR, Mantle Responses SigV4, and NetApp streaming-cost fix to stable/1.88.x and cut 1.88.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;30144](https://github.com/BerriAI/litellm/pull/30144)
- chore(release): backport DB-resilience, passthrough, model-info, budget, and deps fixes to stable/1.88.x by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;30408](https://github.com/BerriAI/litellm/pull/30408)

**Full Changelog**: <https://github.com/BerriAI/litellm/compare/v1.88.1...v1.88.2>

### [`v1.88.2`](https://github.com/BerriAI/litellm/releases/tag/v1.88.2)

[Compare Source](https://github.com/BerriAI/litellm/compare/v1.88.1...v1.88.2)

##### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.88.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

##### What's Changed

- chore(release): backport Fable 5, batch-file auth, CrowdStrike AIDR, Mantle Responses SigV4, and NetApp streaming-cost fix to stable/1.88.x and cut 1.88.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;30144](https://github.com/BerriAI/litellm/pull/30144)
- chore(release): backport DB-resilience, passthrough, model-info, budget, and deps fixes to stable/1.88.x by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;30408](https://github.com/BerriAI/litellm/pull/30408)

**Full Changelog**: <https://github.com/BerriAI/litellm/compare/v1.88.1...v1.88.2>

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/London)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMTkuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIxOS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19-->

Reviewed-on: https://forgejo.hayden.moe/hayden/phoebe/pulls/93
blake-hamm added a commit to blake-hamm/bhamm-lab that referenced this pull request Jun 16, 2026
…to v1.89.0 (#200)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [https://github.com/BerriAI/litellm.git](https://github.com/BerriAI/litellm) | minor | `v1.85.1` → `v1.89.0` |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/155) for more information.

---

### Release Notes

<details>
<summary>BerriAI/litellm (https://github.com/BerriAI/litellm.git)</summary>

### [`v1.89.0`](https://github.com/BerriAI/litellm/releases/tag/v1.89.0)

[Compare Source](https://github.com/BerriAI/litellm/compare/v1.88.2...v1.89.0)

#### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.89.0
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.89.0/cosign.pub \
  ghcr.io/berriai/litellm:v1.89.0
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

#### What's Changed

- test(responses): bump deprecated gemini-3-pro-preview to gemini-3.1-pro-preview by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29433](https://github.com/BerriAI/litellm/pull/29433)
- fix: map mistral/ministral-8b-latest in model price map by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29453](https://github.com/BerriAI/litellm/pull/29453)
- fix(datadog): split oversized batches on 413 instead of re-queueing forever by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29444](https://github.com/BerriAI/litellm/pull/29444)
- feat(otel): allowlist team\_metadata sub-keys promoted to baggage by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29442](https://github.com/BerriAI/litellm/pull/29442)
- fix: stop use\_chat\_completions\_api flag from leaking into provider request body by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29447](https://github.com/BerriAI/litellm/pull/29447)
- fix(anthropic, fireworks): inline legacy $ref defs in tool schemas by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28646](https://github.com/BerriAI/litellm/pull/28646)
- fix(proxy): omit OpenAI \[DONE] on google-genai streamGenerateContent by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29426](https://github.com/BerriAI/litellm/pull/29426)
- ci(release): create stable/X.Y.x line branch on X.Y.0 tags by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29457](https://github.com/BerriAI/litellm/pull/29457)
- fix(vector-stores): support engines URL for Vertex AI Search by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;27885](https://github.com/BerriAI/litellm/pull/27885)
- fix(ui): render caller-supplied filter options in caller order by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29462](https://github.com/BerriAI/litellm/pull/29462)
- fix(batches): skip unnecessary batch input file reads by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29114](https://github.com/BerriAI/litellm/pull/29114)
- docs(agents): clarify when to create new test files by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29472](https://github.com/BerriAI/litellm/pull/29472)
- Litellm OSS Staging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29161](https://github.com/BerriAI/litellm/pull/29161)
- fix(mcp): clear allowed\_tools and tool overrides on MCP server edit by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29411](https://github.com/BerriAI/litellm/pull/29411)
- Litellm OSS Staging 010626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29422](https://github.com/BerriAI/litellm/pull/29422)
- fix(ci): make CircleCI rerun-failed-tests collect tests when 2+ test files fail by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29475](https://github.com/BerriAI/litellm/pull/29475)
- feat(a2a): watsonx Orchestrate agent provider by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29410](https://github.com/BerriAI/litellm/pull/29410)
- fix(azure\_ai): strip tool-level extra fields on 400 and retry by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29479](https://github.com/BerriAI/litellm/pull/29479)
- fix(docs): remove fixed dimensions from README hero image by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29496](https://github.com/BerriAI/litellm/pull/29496)
- Litellm oss staging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29492](https://github.com/BerriAI/litellm/pull/29492)
- fix: small CLAUDE.md nits by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29504](https://github.com/BerriAI/litellm/pull/29504)
- Add MCP semantic conventions to otelv2 by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29468](https://github.com/BerriAI/litellm/pull/29468)
- fix(passthrough): emit otel guardrail span when a guardrail blocks by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29470](https://github.com/BerriAI/litellm/pull/29470)
- fix(proxy): strip NUL bytes from spend log payloads to prevent PostgreSQL 22P05 by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29515](https://github.com/BerriAI/litellm/pull/29515)
- \[internal copy of [#&#8203;28008](https://github.com/BerriAI/litellm/issues/28008)] Support MCP OAuth passthrough and issuer-scoped JWT auth by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28356](https://github.com/BerriAI/litellm/pull/28356)
- feat(vector-stores): forward per-request params to Vertex AI Search by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29459](https://github.com/BerriAI/litellm/pull/29459)
- feat(proxy): add per-MCP-server RPM rate limiting for keys and teams by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29482](https://github.com/BerriAI/litellm/pull/29482)
- fix(tests): drop module-level test calls that break local\_testing collection by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29520](https://github.com/BerriAI/litellm/pull/29520)
- feat(agents): add LangFlow agent provider with A2A session bridging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28963](https://github.com/BerriAI/litellm/pull/28963)
- fix(ui/agents): make A2A skill tags enterable and validated by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29512](https://github.com/BerriAI/litellm/pull/29512)
- \[internal copy of [#&#8203;29232](https://github.com/BerriAI/litellm/issues/29232)] feat: route future Claude models to Anthropic provider via pattern matching by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29239](https://github.com/BerriAI/litellm/pull/29239)
- fix(tests): drop import-time completion call in test\_register\_model by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29521](https://github.com/BerriAI/litellm/pull/29521)
- test: stabilize batch VCR coverage and stop live upload/network leaks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29477](https://github.com/BerriAI/litellm/pull/29477)
- \[internal copy of [#&#8203;29003](https://github.com/BerriAI/litellm/issues/29003)] fix(vertex\_ai): use user-supplied api\_base as is for Model Garden OpenAI-compat path by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29530](https://github.com/BerriAI/litellm/pull/29530)
- feat(proxy): native /health/drain preStop hook for graceful shutdown by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29439](https://github.com/BerriAI/litellm/pull/29439)
- fix(auth): preserve 401 status for expired JWTs in OTel traces by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29510](https://github.com/BerriAI/litellm/pull/29510)
- fix(otel): capture 401 error details in management endpoint spans by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29535](https://github.com/BerriAI/litellm/pull/29535)
- test(proxy/utils): pin bottom-of-file helper behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29509](https://github.com/BerriAI/litellm/pull/29509)
- test(proxy/utils): pin PrismaClient and spend-update behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29488](https://github.com/BerriAI/litellm/pull/29488)
- test(proxy/utils): pin ProxyLogging behavior by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29485](https://github.com/BerriAI/litellm/pull/29485)
- fix: missing span for guardrail passthrough by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29552](https://github.com/BerriAI/litellm/pull/29552)
- fix(auth): let internal users view search tools by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29542](https://github.com/BerriAI/litellm/pull/29542)
- fix: missing mcp otel attributes by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29554](https://github.com/BerriAI/litellm/pull/29554)
- fix(proxy): resolve managed video model ids for auth by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29545](https://github.com/BerriAI/litellm/pull/29545)
- fix(key\_generate): allow team members to create keys on org-scoped teams by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29310](https://github.com/BerriAI/litellm/pull/29310)
- test(pass-through): move Gemini pass-through tests to gemini-3.1-flash-lite by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29595](https://github.com/BerriAI/litellm/pull/29595)
- Litellm oss staging 030626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29578](https://github.com/BerriAI/litellm/pull/29578)
- Fix : a2a bugs 030626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29566](https://github.com/BerriAI/litellm/pull/29566)
- \[internal copy of [#&#8203;29533](https://github.com/BerriAI/litellm/issues/29533)] fix(anthropic/adapter): emit thinking block for reasoning\_content-only streaming chunks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29600](https://github.com/BerriAI/litellm/pull/29600)
- ci: reproduce default-Windows wheel install to guard MAX\_PATH by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29597](https://github.com/BerriAI/litellm/pull/29597)
- fix(vertex): strip output\_config.effort for Vertex Claude models that reject it (Haiku 4.5) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29585](https://github.com/BerriAI/litellm/pull/29585)
- Litellm websocket improvements by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29563](https://github.com/BerriAI/litellm/pull/29563)
- feat(arize/phoenix): OpenInference rendering parity — tool\_calls, cost, passthrough I/O, session/user, multimodal, cache tokens by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28800](https://github.com/BerriAI/litellm/pull/28800)
- \[internal copy of [#&#8203;29550](https://github.com/BerriAI/litellm/issues/29550)] fix: passthrough endpoints duplicate logs by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29598](https://github.com/BerriAI/litellm/pull/29598)
- fix(ci): keep coverage rename green when a parallel node runs no tests by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29608](https://github.com/BerriAI/litellm/pull/29608)
- test(vcr): close out the remaining VCR live-call leaks by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29603](https://github.com/BerriAI/litellm/pull/29603)
- fix(key\_generate): exempt UI/CLI session tokens from the budget ceiling for team keys by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29612](https://github.com/BerriAI/litellm/pull/29612)
- fix(realtime): allow null transcripts in stream logging payloads by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29625](https://github.com/BerriAI/litellm/pull/29625)
- build(ui): migrate eslint to flat config + bump eslint-config-next to 16 by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29626](https://github.com/BerriAI/litellm/pull/29626)
- fix(key\_generate): scope session-token team-key budget exemption to caller-supplied team\_id by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29641](https://github.com/BerriAI/litellm/pull/29641)
- fix(proxy): disable proxy buffering on streaming SSE responses by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29557](https://github.com/BerriAI/litellm/pull/29557)
- fix(mcp): gate /public/mcp\_hub strictly on litellm.public\_mcp\_servers by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;27764](https://github.com/BerriAI/litellm/pull/27764)
- ci(ui): frontend-lint job enforcing prettier + eslint on changed files by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29633](https://github.com/BerriAI/litellm/pull/29633)
- fix(gemini): googleSearch + server-side tools and googleMaps JSON schema by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29582](https://github.com/BerriAI/litellm/pull/29582)
- fix(proxy): passthrough 404 when SERVER\_ROOT\_PATH is set by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29658](https://github.com/BerriAI/litellm/pull/29658)
- fix(gemini-realtime): use GA event names for Pipecat 1.3.x compatibility by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29662](https://github.com/BerriAI/litellm/pull/29662)
- Litellm oss staging 040626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29671](https://github.com/BerriAI/litellm/pull/29671)
- style(ui): prettier formatting pass over the dashboard by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29622](https://github.com/BerriAI/litellm/pull/29622)
- chore: ignore prettier dashboard reformat in git blame by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29695](https://github.com/BerriAI/litellm/pull/29695)
- fix(helm): Enable Backend Deployment to mount Gateway config.yaml by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29605](https://github.com/BerriAI/litellm/pull/29605)
- \[internal copy of [#&#8203;29277](https://github.com/BerriAI/litellm/issues/29277)] fix(proxy): add default=None to LiteLLM\_TeamMembership.litellm\_budget\_table by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29684](https://github.com/BerriAI/litellm/pull/29684)
- test: make custom\_tokenizer proxy tests hermetic by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29643](https://github.com/BerriAI/litellm/pull/29643)
- test(proxy): stop running real-DB tests in GitHub Actions unit jobs by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29700](https://github.com/BerriAI/litellm/pull/29700)
- chore(ui): remove the bare-fetch lint rule by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29712](https://github.com/BerriAI/litellm/pull/29712)
- Litellm jwt mapping virtualkeys by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;28510](https://github.com/BerriAI/litellm/pull/28510)
- refactor(ui): shared HTTP client + location-pinned fetch() lint rule by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29723](https://github.com/BerriAI/litellm/pull/29723)
- fix(proxy): stop team BYOK model name corruption on model edit by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29731](https://github.com/BerriAI/litellm/pull/29731)
- \[internal copy of [#&#8203;29511](https://github.com/BerriAI/litellm/issues/29511)] feat(guardrails): add sensitive data routing to on-premise models by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29531](https://github.com/BerriAI/litellm/pull/29531)
- fix(proxy/hooks): populate llm\_provider on internal rate-limit errors by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;27707](https://github.com/BerriAI/litellm/pull/27707)
- fix(vertex/anthropic): handle namespace tools and strip client\_metadata for codex compatibility by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29489](https://github.com/BerriAI/litellm/pull/29489)
- Support OAuth M2M for Databricks Apps A2A agents by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29586](https://github.com/BerriAI/litellm/pull/29586)
- fix: small CLAUDE.md nit by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29749](https://github.com/BerriAI/litellm/pull/29749)
- fix(anthropic): route Claude Opus 4.8 through adaptive thinking by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29702](https://github.com/BerriAI/litellm/pull/29702)
- fix(proxy): persist oauth2\_flow on MCP server registration by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;29690](https://github.com/BerriAI/litellm/pull/29690)
- \[internal copy of [#&#8203;27491](https://github.com/BerriAI/litellm/issues/27491)] fix(realtime): Fix Realtime Audio Token Cost Tracking by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29722](https://github.com/BerriAI/litellm/pull/29722)
- fix(galileo): use ingest traces API and standard logging payload by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29651](https://github.com/BerriAI/litellm/pull/29651)
- fix(auth): expand all-team-models sentinel in can\_key\_call\_model for batch validation by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29746](https://github.com/BerriAI/litellm/pull/29746)
- test(vcr): stop refreshing cassette TTL on read so cassettes lapse after 24h by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29784](https://github.com/BerriAI/litellm/pull/29784)
- test(ci): record/replay OpenAI image gen so the spend E2E isn't outage-bound by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29787](https://github.com/BerriAI/litellm/pull/29787)
- fix(ui): route MCP playground auth by oauth2 mode instead of token\_url by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29714](https://github.com/BerriAI/litellm/pull/29714)
- refactor(ui): centralize proxy base URL resolution into tested resolver by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29793](https://github.com/BerriAI/litellm/pull/29793)
- Litellm oss staging 050626 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29774](https://github.com/BerriAI/litellm/pull/29774)
- test(google): add google-genai SDK proxy integration tests by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29781](https://github.com/BerriAI/litellm/pull/29781)
- fix(jwt): use resolved DB user\_id for spend on legacy email match by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29217](https://github.com/BerriAI/litellm/pull/29217)
- feat(ui): generate dashboard API types from the proxy OpenAPI spec by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29816](https://github.com/BerriAI/litellm/pull/29816)
- fix(proxy): drop deleted team BYOK model name from team.models by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29820](https://github.com/BerriAI/litellm/pull/29820)
- feat(mcp): per-server env vars with global + per-user scopes by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28917](https://github.com/BerriAI/litellm/pull/28917)
- refactor(ui): route behavior-preserving networking calls through apiClient by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29806](https://github.com/BerriAI/litellm/pull/29806)
- fix(mcp): persist Tools-tab MCP OAuth token to DB by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29809](https://github.com/BerriAI/litellm/pull/29809)
- fix(ui): require new expiration when regenerating an expired key by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;29838](https://github.com/BerriAI/litellm/pull/29838)
- refactor(ui): route query-building networking calls through apiClient by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29815](https://github.com/BerriAI/litellm/pull/29815)
- Make the image-gen record/replay proxy report cache mode and per-request HIT/MISS by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29802](https://github.com/BerriAI/litellm/pull/29802)
- feat(proxy): hot-reload .env in dev when running with --reload by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29783](https://github.com/BerriAI/litellm/pull/29783)
- fix(ui): stop MCP playground tool calls from sending twice by [@&#8203;tin-berri](https://github.com/tin-berri) in [#&#8203;29821](https://github.com/BerriAI/litellm/pull/29821)
- feat(fal\_ai): add Nano Banana / Gemini 2.5 Flash Image generation support by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29798](https://github.com/BerriAI/litellm/pull/29798)
- Title: Fix managed batch cancel credential resolution by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29734](https://github.com/BerriAI/litellm/pull/29734)
- Title: fix(proxy): resolve vector store file list credentials from team deployments by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29739](https://github.com/BerriAI/litellm/pull/29739)
- refactor: convert AWS and GCP Terraform stacks into reusable modules … by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28103](https://github.com/BerriAI/litellm/pull/28103)
- chore(ui): build ui for release by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29853](https://github.com/BerriAI/litellm/pull/29853)
- fix(terraform/gcp): prompt for image\_registry in DeployStack one-click by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29852](https://github.com/BerriAI/litellm/pull/29852)
- fix(terraform/gcp): abandon SQL user on destroy by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29855](https://github.com/BerriAI/litellm/pull/29855)
- Extend the record/replay proxy to chat, embeddings, moderations, rerank, and Anthropic by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29847](https://github.com/BerriAI/litellm/pull/29847)
- chore(deps): bump deps by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29860](https://github.com/BerriAI/litellm/pull/29860)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29861](https://github.com/BerriAI/litellm/pull/29861)
- fix: 400 on Anthropic context overflow; seed identity on failed auth by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29848](https://github.com/BerriAI/litellm/pull/29848)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29862](https://github.com/BerriAI/litellm/pull/29862)
- chore(release): patch v1.89.0-rc.1 with [#&#8203;30064](https://github.com/BerriAI/litellm/issues/30064) (Claude Fable 5) for v1.89.0-rc.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;30143](https://github.com/BerriAI/litellm/pull/30143)

**Full Changelog**: <https://github.com/BerriAI/litellm/compare/v1.88.0...v1.89.0>

### [`v1.88.2`](https://github.com/BerriAI/litellm/releases/tag/v1.88.2)

[Compare Source](https://github.com/BerriAI/litellm/compare/v1.88.1...v1.88.2)

#### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.88.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

#### What's Changed

- chore(release): backport Fable 5, batch-file auth, CrowdStrike AIDR, Mantle Responses SigV4, and NetApp streaming-cost fix to stable/1.88.x and cut 1.88.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;30144](https://github.com/BerriAI/litellm/pull/30144)
- chore(release): backport DB-resilience, passthrough, model-info, budget, and deps fixes to stable/1.88.x by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;30408](https://github.com/BerriAI/litellm/pull/30408)

**Full Changelog**: <https://github.com/BerriAI/litellm/compare/v1.88.1...v1.88.2>

### [`v1.88.1`](https://github.com/BerriAI/litellm/releases/tag/v1.88.1)

[Compare Source](https://github.com/BerriAI/litellm/compare/v1.88.0...v1.88.1)

#### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.1
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.88.1/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.1
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

#### What's Changed

- build(deps): bump pyjwt to 2.13.0 and ws override to 8.20.1 (1.88.x) by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29987](https://github.com/BerriAI/litellm/pull/29987)
- chore(release): bump version to 1.88.1 by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29989](https://github.com/BerriAI/litellm/pull/29989)

**Full Changelog**: <https://github.com/BerriAI/litellm/compare/v1.88.0...v1.88.1>

### [`v1.88.0`](https://github.com/BerriAI/litellm/releases/tag/v1.88.0)

[Compare Source](https://github.com/BerriAI/litellm/compare/v1.87.3...v1.88.0)

#### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](https://github.com/BerriAI/litellm/commit/0112e53046018d726492c814b3644b7d376029d0).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.0
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.88.0/cosign.pub \
  ghcr.io/berriai/litellm:v1.88.0
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

#### What's Changed

- fix(proxy): gate team allowed\_passthrough\_routes to proxy admins by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28097](https://github.com/BerriAI/litellm/pull/28097)
- fix(tests): stabilize image-edit VCR cassettes to stop live gpt-image-1 spend by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28110](https://github.com/BerriAI/litellm/pull/28110)
- fix(bedrock/cohere): send embedding\_types as JSON array, not string by [@&#8203;ishaan-berri](https://github.com/ishaan-berri) in [#&#8203;28172](https://github.com/BerriAI/litellm/pull/28172)
- fix(tests): migrate realtime + rerank tests off shut-down upstream models by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28191](https://github.com/BerriAI/litellm/pull/28191)
- fix(caching): replay openai/responses bridge cache hits as chat streams by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28158](https://github.com/BerriAI/litellm/pull/28158)
- Litellm oss staging by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28161](https://github.com/BerriAI/litellm/pull/28161)
- feat(prometheus): add user\_email and user\_alias to user budget metrics by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28155](https://github.com/BerriAI/litellm/pull/28155)
- test(callbacks): harden flaky proxy callback-leak detector by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28195](https://github.com/BerriAI/litellm/pull/28195)
- fix(bedrock): sanitize batch metadata to prevent Pydantic ValidationError by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28202](https://github.com/BerriAI/litellm/pull/28202)
- fix(deepseek): use native /anthropic/v1/messages endpoint and sanitize tools by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28200](https://github.com/BerriAI/litellm/pull/28200)
- feat(ui): add Interactions API endpoint to playground with SSE streaming by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28156](https://github.com/BerriAI/litellm/pull/28156)
- fix(proxy): decode bytes and pass-through SSE for Google-native streamGenerateContent ([#&#8203;27444](https://github.com/BerriAI/litellm/issues/27444)) by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28213](https://github.com/BerriAI/litellm/pull/28213)
- refactor(bedrock/sagemaker): switch to lazy loading for response stre… by [@&#8203;harish-berri](https://github.com/harish-berri) in [#&#8203;28189](https://github.com/BerriAI/litellm/pull/28189)
- \[Refactor] UI - Spend Logs: consolidate filter state and extract components by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;25847](https://github.com/BerriAI/litellm/pull/25847)
- fix(tests): replace shut-down gpt-4o-audio-preview with gpt-audio-1.5 by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28281](https://github.com/BerriAI/litellm/pull/28281)
- chore(ci): bump versions by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28287](https://github.com/BerriAI/litellm/pull/28287)
- feat: propagate team\_id and team\_alias to all child OTEL spans by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28273](https://github.com/BerriAI/litellm/pull/28273)
- Day 0 support : Gemini 3.5 Flash by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28268](https://github.com/BerriAI/litellm/pull/28268)
- Gemini managed agents support by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28270](https://github.com/BerriAI/litellm/pull/28270)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28292](https://github.com/BerriAI/litellm/pull/28292)
- feat(gemini): add gemini-3.1-flash-lite model cost map by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28320](https://github.com/BerriAI/litellm/pull/28320)
- fix(spend\_counter): seed Redis counter via SET NX to prevent cross-pod double-seed by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;27854](https://github.com/BerriAI/litellm/pull/27854)
- fix(proxy): normalize batch file IDs before ManagedObjectTable write by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28339](https://github.com/BerriAI/litellm/pull/28339)
- fix(router): use forwarded model\_id for native Azure container IDs by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;27921](https://github.com/BerriAI/litellm/pull/27921)
- fix(ui): restore log filter loading indicator by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28282](https://github.com/BerriAI/litellm/pull/28282)
- test(e2e): migrate runner to uv, add All Proxy Models key test by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28313](https://github.com/BerriAI/litellm/pull/28313)
- feat(ui): team passthrough routes create parity + edit load fix by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28098](https://github.com/BerriAI/litellm/pull/28098)
- fix(mcp): JWT on tools/list and REST tools/call server resolution by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28227](https://github.com/BerriAI/litellm/pull/28227)
- feat(interactions): migrate to Google Interactions API steps schema (May 2026) by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28153](https://github.com/BerriAI/litellm/pull/28153)
- test(ui-e2e): admin key creation with a specific proxy model by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28365](https://github.com/BerriAI/litellm/pull/28365)
- fix(vertex\_ai): omit function\_call id on Vertex Gemini 3.5+ tool turns by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28324](https://github.com/BerriAI/litellm/pull/28324)
- feat(mcp): allow native MCP OAuth support for cursor by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28327](https://github.com/BerriAI/litellm/pull/28327)
- fix(interactions): never drop streamed text deltas; always emit terminal completion by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28394](https://github.com/BerriAI/litellm/pull/28394)
- fix(proxy): expose Prisma idle/connect timeout + extra DB URL params by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28395](https://github.com/BerriAI/litellm/pull/28395)
- Litellm oss staging 1 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28337](https://github.com/BerriAI/litellm/pull/28337)
- fix: serialize guardrail\_response to JSON in OTEL traces by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28362](https://github.com/BerriAI/litellm/pull/28362)
- chore(ci): merge dev branch by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28314](https://github.com/BerriAI/litellm/pull/28314)
- test(realtime): expect session.created as xAI realtime initial event by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28424](https://github.com/BerriAI/litellm/pull/28424)
- feat(tests): behavior-pinning harness + Key Tier-1 matrix by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28321](https://github.com/BerriAI/litellm/pull/28321)
- fix(proxy): hydrate wildcard discovery credentials ([#&#8203;28284](https://github.com/BerriAI/litellm/issues/28284)) - CCI Run by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28419](https://github.com/BerriAI/litellm/pull/28419)
- Litellm oss staging 04 21 2026 2 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;26569](https://github.com/BerriAI/litellm/pull/26569)
- chore(ci): merge dev branch by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28290](https://github.com/BerriAI/litellm/pull/28290)
- fix(vertex\_gemma): strip `context_management` from request body by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28438](https://github.com/BerriAI/litellm/pull/28438)
- fix(logging): recalculate cost after router retry failures by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28476](https://github.com/BerriAI/litellm/pull/28476)
- fix(otel): emit guardrail span on violation, surface status + categories by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28364](https://github.com/BerriAI/litellm/pull/28364)
- test(proxy): behavior-pinning matrix for team management endpoints by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28441](https://github.com/BerriAI/litellm/pull/28441)
- test(vertex\_ai): tolerate transient 500 in google maps grounding test by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28503](https://github.com/BerriAI/litellm/pull/28503)
- fix(docker): restore npm to non\_root builder image by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28519](https://github.com/BerriAI/litellm/pull/28519)
- chore(ci): bump deps by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28524](https://github.com/BerriAI/litellm/pull/28524)
- build(deps-dev): bump black to 26.3.1 and apply formatting by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28525](https://github.com/BerriAI/litellm/pull/28525)
- chore(deps): bump deps by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28528](https://github.com/BerriAI/litellm/pull/28528)
- test(e2e): forward LITELLM\_LICENSE to UI e2e proxy by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28398](https://github.com/BerriAI/litellm/pull/28398)
- Add granian as a ASGI compliant web server. Provider better throughput stability, by [@&#8203;harish-berri](https://github.com/harish-berri) in [#&#8203;26027](https://github.com/BerriAI/litellm/pull/26027)
- Fix conflicts and UI by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28477](https://github.com/BerriAI/litellm/pull/28477)
- Add error\_description and hint for oauth flows by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28471](https://github.com/BerriAI/litellm/pull/28471)
- feat(mcp): Add tool call and tool list support via UI for Oauth mcps by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28454](https://github.com/BerriAI/litellm/pull/28454)
- feat(proxy): persist allowlisted OIDC claims in CLI SSO poll by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28463](https://github.com/BerriAI/litellm/pull/28463)
- fix(responses): use OpenAI SSEDecoder for Responses API streaming by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28566](https://github.com/BerriAI/litellm/pull/28566)
- Litellm oss staging 2 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28582](https://github.com/BerriAI/litellm/pull/28582)
- \[internal copy of [#&#8203;28269](https://github.com/BerriAI/litellm/issues/28269)] Codex cli jwt team alias by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28621](https://github.com/BerriAI/litellm/pull/28621)
- fix(check\_licenses): read PEP 639 license-expression metadata by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28529](https://github.com/BerriAI/litellm/pull/28529)
- test(proxy): behavior-pinning matrix for tier-2/3 key + team management endpoints by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28620](https://github.com/BerriAI/litellm/pull/28620)
- chore(test): remove dead old Playwright e2e suite by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28632](https://github.com/BerriAI/litellm/pull/28632)
- fix(sagemaker): send native Cohere embed payload to Cohere SageMaker endpoints by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28613](https://github.com/BerriAI/litellm/pull/28613)
- style: apply black formatting to fix lint CI (LIT-3274) ([#&#8203;28639](https://github.com/BerriAI/litellm/issues/28639)) by [@&#8203;krrish-berri-2](https://github.com/krrish-berri-2) in [#&#8203;28641](https://github.com/BerriAI/litellm/pull/28641)
- fix(bedrock): decouple STS region from Bedrock aws\_region\_name by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28245](https://github.com/BerriAI/litellm/pull/28245)
- test(streaming): tolerate Vertex 429 wrapped in MidStreamFallbackError by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28669](https://github.com/BerriAI/litellm/pull/28669)
- feat(guardrails): add Microsoft Purview DLP guardrail by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;24966](https://github.com/BerriAI/litellm/pull/24966)
- fix(mcp): forward upstream initialize instructions on cold gateway init by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;28231](https://github.com/BerriAI/litellm/pull/28231)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28680](https://github.com/BerriAI/litellm/pull/28680)
- CI: copy of [#&#8203;25177](https://github.com/BerriAI/litellm/issues/25177) (OCI GenAI: embeddings, streaming/reasoning fixes, model catalog) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28223](https://github.com/BerriAI/litellm/pull/28223)
- Encrypt callback\_vars in key/team metadata in DB by [@&#8203;Michael-RZ-Berri](https://github.com/Michael-RZ-Berri) in [#&#8203;27141](https://github.com/BerriAI/litellm/pull/27141)
- perf: reduce per-request and per-chunk overhead across Anthropic streaming hot paths by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28289](https://github.com/BerriAI/litellm/pull/28289)
- feat(azure): add Speech STT config support by [@&#8203;ishaan-berri](https://github.com/ishaan-berri) in [#&#8203;27482](https://github.com/BerriAI/litellm/pull/27482)
- test(proxy): phase-4 payload behavior pinning for tier-2/3 key + team management endpoints by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28681](https://github.com/BerriAI/litellm/pull/28681)
- feat(prometheus): emit per-token-type detail metrics (LIT-3220) ([#&#8203;28372](https://github.com/BerriAI/litellm/issues/28372)) by [@&#8203;ishaan-berri](https://github.com/ishaan-berri) in [#&#8203;28378](https://github.com/BerriAI/litellm/pull/28378)
- fix(otel): stamp http.response.status\_code on all error responses by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28405](https://github.com/BerriAI/litellm/pull/28405)
- chore(ui): build ui by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28707](https://github.com/BerriAI/litellm/pull/28707)
- fix(helm): drop main- prefix from default image tag by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28710](https://github.com/BerriAI/litellm/pull/28710)
- test(model\_prices): allow audio\_transcription\_config in schema by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28708](https://github.com/BerriAI/litellm/pull/28708)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28709](https://github.com/BerriAI/litellm/pull/28709)
- fix(team): refresh team cache on team\_model\_add/delete (LIT-3244) by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28683](https://github.com/BerriAI/litellm/pull/28683)
- fix(ui/add-model): stop vertex\_ai-anthropic\_models from leaking into Anthropic dropdown by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28723](https://github.com/BerriAI/litellm/pull/28723)
- Fix spend logs v2 route permissions by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28705](https://github.com/BerriAI/litellm/pull/28705)
- fix(proxy): Bedrock Knowledge Base pass-through: preserve SigV4 headers and signed request body by [@&#8203;milan-berri](https://github.com/milan-berri) in [#&#8203;27526](https://github.com/BerriAI/litellm/pull/27526)
- chore(tests): migrate Bedrock CI to AWS account [`9412775`](https://github.com/BerriAI/litellm/commit/941277531214) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28728](https://github.com/BerriAI/litellm/pull/28728)
- fix(otel): export SERVER span on management-endpoint success without http\_request by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28794](https://github.com/BerriAI/litellm/pull/28794)
- chore(ci): merge dev branch by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28801](https://github.com/BerriAI/litellm/pull/28801)
- chore(ci): merge dev branch by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28657](https://github.com/BerriAI/litellm/pull/28657)
- fix(ui): show 2-decimal precision for max\_budget on key overview by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28809](https://github.com/BerriAI/litellm/pull/28809)
- feat(proxy): allow `llm_api_routes` virtual keys to list MCP servers by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28442](https://github.com/BerriAI/litellm/pull/28442)
- chore(ci): merge dev branch by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28807](https://github.com/BerriAI/litellm/pull/28807)
- fix(team): keep team\_alias cache in sync on \_cache\_team\_object writes by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28737](https://github.com/BerriAI/litellm/pull/28737)
- chore(ci): merge dev branch by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28822](https://github.com/BerriAI/litellm/pull/28822)
- ci: daily oss-agent-shin canonical branch by [@&#8203;ishaan-berri](https://github.com/ishaan-berri) in [#&#8203;28829](https://github.com/BerriAI/litellm/pull/28829)
- test(proxy): add harness for proxy\_server.py behavior-pinning by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28827](https://github.com/BerriAI/litellm/pull/28827)
- feat(openai): apply regional-processing cost uplift for EU/US data residency by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28626](https://github.com/BerriAI/litellm/pull/28626)
- chore(admin-ui): regenerate static export with trailingSlash: true by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28112](https://github.com/BerriAI/litellm/pull/28112)
- fix(azure): preserve AD token refresh in v1 OpenAI client path by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28627](https://github.com/BerriAI/litellm/pull/28627)
- fix(ui): route API Reference back to query-param page by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28726](https://github.com/BerriAI/litellm/pull/28726)
- fix(model-edit): allow clearing custom pricing on wildcard models by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28719](https://github.com/BerriAI/litellm/pull/28719)
- fix(tests/vcr): make Redis cassette cache replay deterministically (zero VCR misses on consecutive runs) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28826](https://github.com/BerriAI/litellm/pull/28826)
- fix(proxy): strip LiteLLM policy tracking from OpenAI batch metadata by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;28425](https://github.com/BerriAI/litellm/pull/28425)
- Litellm OpenAI double prefix bug by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;28661](https://github.com/BerriAI/litellm/pull/28661)
- Litellm oss staging 250526 by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28770](https://github.com/BerriAI/litellm/pull/28770)
- fix(bedrock): align toolUse/toolSpec names and allow hyphens by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28874](https://github.com/BerriAI/litellm/pull/28874)
- fix(realtime): send TEXT frames and valid guardrail session.update by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28848](https://github.com/BerriAI/litellm/pull/28848)
- fix(mcp): extend key access-group union to MCP servers by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28890](https://github.com/BerriAI/litellm/pull/28890)
- fix(galileo): support hosted v2 spans API and string output extraction by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28771](https://github.com/BerriAI/litellm/pull/28771)
- fix(proxy): exclude proxy\_server\_request from its own body snapshot by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;28618](https://github.com/BerriAI/litellm/pull/28618)
- \[Feat] Add tool calling support for gemini and vertex ai live api by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;26590](https://github.com/BerriAI/litellm/pull/26590)
- refactor(ui): remove dead App Router scaffolding in (dashboard)/\* by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28891](https://github.com/BerriAI/litellm/pull/28891)
- fix(docker): use system Node in componentized builders + retry apk add by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28888](https://github.com/BerriAI/litellm/pull/28888)
- docs(agents): require consent before writing new third-party names by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;28908](https://github.com/BerriAI/litellm/pull/28908)
- refactor(ui): extract auth state into AuthContext by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28910](https://github.com/BerriAI/litellm/pull/28910)
- fix(mcp): resolve team.access\_group\_ids → MCP servers by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28997](https://github.com/BerriAI/litellm/pull/28997)
- test(ui): e2e cover team model edit + admin identity in navbar by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;28652](https://github.com/BerriAI/litellm/pull/28652)
- test(e2e): cover add-fallback flow in Router Settings by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29069](https://github.com/BerriAI/litellm/pull/29069)
- test(e2e): cover Team-BYOK add-model flow as proxy admin by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29068](https://github.com/BerriAI/litellm/pull/29068)
- fix(containers): record ownership for service-account keys + fix Prisma Json serialization by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28990](https://github.com/BerriAI/litellm/pull/28990)
- test(e2e): cover add-MCP-server flow via discovery → custom form by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29070](https://github.com/BerriAI/litellm/pull/29070)
- test(e2e): cover AI Hub make-public flow and public model\_hub\_table by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29071](https://github.com/BerriAI/litellm/pull/29071)
- \[internal copy of [#&#8203;28877](https://github.com/BerriAI/litellm/issues/28877)] feat: add support for claude code goal mode for bedrock opus output config by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;28898](https://github.com/BerriAI/litellm/pull/28898)
- feat(guardrails): wire apply\_guardrail into proxy logging callbacks by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28970](https://github.com/BerriAI/litellm/pull/28970)
- chore(ci): merge dev brach by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29192](https://github.com/BerriAI/litellm/pull/29192)
- perf(streaming): cut per-chunk overhead \~30% on Anthropic + Bedrock hot path by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28720](https://github.com/BerriAI/litellm/pull/28720)
- fix(proxy): enforce tag budgets for key-level tags by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29108](https://github.com/BerriAI/litellm/pull/29108)
- fix(vertex-ai): use DB credentials in video handlers + implement Veo video edit by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29098](https://github.com/BerriAI/litellm/pull/29098)
- fix(datadog): drain cost-management queue + opt-in FinOps tag allowlist by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;28487](https://github.com/BerriAI/litellm/pull/28487)
- feat(helm): split per-component ServiceAccounts for gateway, backend, and UI by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28712](https://github.com/BerriAI/litellm/pull/28712)
- chore(ci): bump deps ([#&#8203;29208](https://github.com/BerriAI/litellm/issues/29208)) by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29226](https://github.com/BerriAI/litellm/pull/29226)
- fix(tests/vcr): mint Google OAuth tokens live to prevent stale-token replay by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29229](https://github.com/BerriAI/litellm/pull/29229)
- chore(cookbook): bump Go directive to 1.26.3 in gollem example by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29234](https://github.com/BerriAI/litellm/pull/29234)
- chore(ci): bump version by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29242](https://github.com/BerriAI/litellm/pull/29242)
- feat(anthropic): add Claude Opus 4.8 and prune reasoning-effort flags by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29238](https://github.com/BerriAI/litellm/pull/29238)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29243](https://github.com/BerriAI/litellm/pull/29243)
- fix(ci): restore real Bedrock batch S3 bucket/role in oai\_misc\_config by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29245](https://github.com/BerriAI/litellm/pull/29245)
- fix(guardrails): persist disable\_global\_guardrails on keys by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29233](https://github.com/BerriAI/litellm/pull/29233)
- test(e2e): cover Team Admin view + member + key flows by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29072](https://github.com/BerriAI/litellm/pull/29072)
- docs: hand-written CLAUDE.md; remove AGENTS.md, point GEMINI.md at it by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29252](https://github.com/BerriAI/litellm/pull/29252)
- fix(teams): expose keys\_count on /v2/team/list and wire UI Resources badge by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;28502](https://github.com/BerriAI/litellm/pull/28502)
- fix(anthropic): stop injecting unsupported output\_config.effort=xhigh for Claude Code on Sonnet/Opus 4.6 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29304](https://github.com/BerriAI/litellm/pull/29304)
- test(e2e): cover Internal Viewer nav, key, and team-info gating by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29075](https://github.com/BerriAI/litellm/pull/29075)
- test(e2e): cover Internal User key modal, team info, key page by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29074](https://github.com/BerriAI/litellm/pull/29074)
- test(e2e): cover navbar Logout flow as proxy admin by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29076](https://github.com/BerriAI/litellm/pull/29076)
- fix(mcp): resolve key.access\_group\_ids → MCP servers (ungated) by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29195](https://github.com/BerriAI/litellm/pull/29195)
- fix(router): enforce deployment budgets for dynamically added models by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29273](https://github.com/BerriAI/litellm/pull/29273)
- fix(proxy): map stripped batch body.model to proxy alias for auth by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29264](https://github.com/BerriAI/litellm/pull/29264)
- feat(mcp): support stateless and stateful clients via session-id routing by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;26857](https://github.com/BerriAI/litellm/pull/26857)
- fix(bedrock): support tool search results + chat annotations by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29120](https://github.com/BerriAI/litellm/pull/29120)
- fix(mcp): ignore stale ids on key save by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29128](https://github.com/BerriAI/litellm/pull/29128)
- feat(a2a): well-known agent-card discovery + LangGraph Platform mode by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28860](https://github.com/BerriAI/litellm/pull/28860)
- fix(proxy): link passthrough success spans to the SERVER root OTEL span by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29315](https://github.com/BerriAI/litellm/pull/29315)
- \[internal copy of [#&#8203;29089](https://github.com/BerriAI/litellm/issues/29089)] fix: duplicate claude code traces by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29311](https://github.com/BerriAI/litellm/pull/29311)
- feat(otel): typed semconv-aligned OpenTelemetry instrumentation by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;28909](https://github.com/BerriAI/litellm/pull/28909)
- tests(proxy\_server): surface current behavior in tests by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29309](https://github.com/BerriAI/litellm/pull/29309)
- test(e2e): cover Internal User create-key flow when in no teams by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29083](https://github.com/BerriAI/litellm/pull/29083)
- test(e2e): assert internal-user navbar identity is scoped to that user by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29077](https://github.com/BerriAI/litellm/pull/29077)
- feat(otel): add team\_metadata, http.route, and model names to inference spans by [@&#8203;yassin-berriai](https://github.com/yassin-berriai) in [#&#8203;29319](https://github.com/BerriAI/litellm/pull/29319)
- feat(context\_management): compact\_20260112 polyfill for non-Anthropic providers by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;28868](https://github.com/BerriAI/litellm/pull/28868)
- feat(enterprise): add RESEND\_FROM\_EMAIL for self-hosted Resend sends by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;28830](https://github.com/BerriAI/litellm/pull/28830)
- Revert Bedrock CI back to the reactivated AWS account ([`8886022`](https://github.com/BerriAI/litellm/commit/888602223428)) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29326](https://github.com/BerriAI/litellm/pull/29326)
- fix(mcp): preserve source\_url in GET /v1/mcp/server list responses by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29249](https://github.com/BerriAI/litellm/pull/29249)
- fix(mcp): preserve omitted fields on PUT /v1/mcp/server partial updates by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29253](https://github.com/BerriAI/litellm/pull/29253)
- fix(ci): make litellm\_internal\_staging green (logging test + Bedrock Opus 4.7 self-heal) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29344](https://github.com/BerriAI/litellm/pull/29344)
- refactor(proxy/auth): normalize Bearer prefix in safe-hash helper by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29343](https://github.com/BerriAI/litellm/pull/29343)
- test(reasoning-effort-grid): cover Claude Opus 4.8 across provider routes by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29327](https://github.com/BerriAI/litellm/pull/29327)
- fix(guardrails): return HTTP 400 for litellm content filter blocks by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;28418](https://github.com/BerriAI/litellm/pull/28418)
- fix(proxy): restrict vector store index create/delete to proxy admins by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29202](https://github.com/BerriAI/litellm/pull/29202)
- feat(pass\_through): extend passthrough\_managed\_object\_ids to Azure by [@&#8203;Sameerlite](https://github.com/Sameerlite) in [#&#8203;29160](https://github.com/BerriAI/litellm/pull/29160)
- fix(proxy): enforce allowed\_passthrough\_routes for auth=true pass-thr… by [@&#8203;shivamrawat1](https://github.com/shivamrawat1) in [#&#8203;29256](https://github.com/BerriAI/litellm/pull/29256)
- feat(mcp/auth): additive key access-group grants + opt-in member assignment by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29313](https://github.com/BerriAI/litellm/pull/29313)
- fix(reset\_budget): write only {spend, budget\_reset\_at} and stop pre-zeroing counter by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29358](https://github.com/BerriAI/litellm/pull/29358)
- test(e2e): cover PROXY\_LOGOUT\_URL redirect on Logout by [@&#8203;ryan-crabbe-berri](https://github.com/ryan-crabbe-berri) in [#&#8203;29080](https://github.com/BerriAI/litellm/pull/29080)
- fix(ui): break logout redirect loop across dev and proxy origins by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29360](https://github.com/BerriAI/litellm/pull/29360)
- fix(openai-moderation): wire streaming flags through to unified dispatcher by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;27324](https://github.com/BerriAI/litellm/pull/27324)
- chore(ci): build ui by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29366](https://github.com/BerriAI/litellm/pull/29366)
- fix(v3 limiter): cap no-max\_tokens TPM floor at smallest configured limit by [@&#8203;michelligabriele](https://github.com/michelligabriele) in [#&#8203;28805](https://github.com/BerriAI/litellm/pull/28805)
- fix(e2e): tolerate trailing slash in SERVER\_ROOT\_PATH login redirect by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29369](https://github.com/BerriAI/litellm/pull/29369)
- chore(deps): bump deps by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29373](https://github.com/BerriAI/litellm/pull/29373)
- chore(ci): promote internal staging to main by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;29372](https://github.com/BerriAI/litellm/pull/29372)
- chore(release): patch v1.88.0-rc.1 with four staged fixes by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29632](https://github.com/BerriAI/litellm/pull/29632)
- chore(release): patch v1.88.0-rc.1 with [#&#8203;29612](https://github.com/BerriAI/litellm/issues/29612) (session-token budget-ceiling exemption) by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;29637](https://github.com/BerriAI/litellm/pull/29637)
- fix(key\_generate): harden GHSA-q775 …
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…erriAI#29411)

* fix(mcp): clear allowed_tools and tool overrides on MCP server edit

Send empty arrays/objects from the dashboard instead of null, coerce legacy
null payloads before Prisma, and stop auto-selecting all tools when the
stored allowlist is empty.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(mcp): simplify CRUD panel value ternary per review

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): enforce empty tool allowlist when cleared in dashboard

Set mcp_info.tool_allowlist_enforced on UI save so [] blocks all tools
while legacy servers with default [] remain unrestricted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix legacy MCP tool allowlist edit state

* test(mcp): pin allowlist fields on mock server in tools test

MagicMock auto-attributes are truthy and trigger server_applies_tool_allowlist
after the empty-allowlist enforcement change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): avoid locking legacy servers on quick edit save

Only set tool_allowlist_enforced when already enforced or the user
selected tools; skip allowlist fields on save for unrestricted servers;
do not auto-select all tools when editing legacy servers before load.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): type mcp_info base for allowlist flag read

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): use MCPInfo type for tool_allowlist_enforced in edit save

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* Remove unused MCP allowlist variable

* Fix MCP legacy tool state display

* Fix legacy MCP tool allowlist saves

* fix(mcp): enforce allowlist when create flow deselects all tools

Track explicit allowlist interaction in the create form so deselecting
every tool persists tool_allowlist_enforced=true. Previously an empty
selection sent the flag as false with allowed_tools=[], which the proxy
treats as allow-all, contradicting the UI's 0 tools enabled state. This
mirrors the existing edit-flow handling.

* fix(mcp): enforce disallowed_tools on REST listing and keep restored tool selection on legacy edit

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants