From e7456e8a0fb5beb45382ccee433f2cbc34a0b7f0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:03:56 +0000 Subject: [PATCH] fix(mcp): limit bare delete-verb precedence to leading or conjoined verbs Co-Authored-By: bot_apk --- .../mcp_server/tool_classification.py | 21 ++++++++++++++----- .../mcp_tool_classification_cases.json | 5 +++-- .../src/utils/mcpToolCrudClassification.ts | 7 ++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/tool_classification.py b/litellm/proxy/_experimental/mcp_server/tool_classification.py index 34342fb075b..80be98e2726 100644 --- a/litellm/proxy/_experimental/mcp_server/tool_classification.py +++ b/litellm/proxy/_experimental/mcp_server/tool_classification.py @@ -116,9 +116,19 @@ def _token_variants(token: str) -> frozenset[str]: return frozenset(variant for variant in variants if variant) +_CONJUNCTION_TOKENS: Final = frozenset({"and", "then", "or", "n"}) + + +def _has_bare_delete_verb(tokens: tuple[str, ...]) -> bool: + return any( + token in _DELETE_TOKENS and (index == 0 or tokens[index - 1] in _CONJUNCTION_TOKENS) + for index, token in enumerate(tokens) + ) + + def _classify_tokens(tokens: Iterable[str]) -> ToolOperation: token_tuple: Final = tuple(tokens) - if frozenset(token_tuple) & _DELETE_TOKENS: + if _has_bare_delete_verb(token_tuple): return "delete" token_set: Final = frozenset(chain.from_iterable(map(_token_variants, token_tuple))) if token_set & _READ_TOKENS: @@ -136,10 +146,11 @@ def classify_tool_op(name: str, description: str | None = None) -> ToolOperation """Classify a tool by exact token match on its name, falling back to the description's words only when the name yields no recognized token. - A bare destructive verb outranks read tokens, so ``get_and_delete_item`` - is delete while inflected forms only match through variants: precedence - is then read > delete > update > create, keeping ``get_removed_entries`` - read. A misleading description cannot override a recognized name.""" + A bare destructive verb outranks read tokens only when it leads the name + or follows a conjunction, so ``get_and_delete_item`` is delete while + ``describe_purge_job`` and ``getDeleteStatus`` are read. Inflected forms + only match through variants under read > delete > update > create, and a + misleading description cannot override a recognized name.""" by_name: Final = _classify_tokens(_name_tokens(name)) if by_name != "unknown": return by_name diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/fixtures/mcp_tool_classification_cases.json b/tests/test_litellm/proxy/_experimental/mcp_server/fixtures/mcp_tool_classification_cases.json index ebfecfe3c19..7f0652f27b7 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/fixtures/mcp_tool_classification_cases.json +++ b/tests/test_litellm/proxy/_experimental/mcp_server/fixtures/mcp_tool_classification_cases.json @@ -9,7 +9,7 @@ {"name": "get_removed_entries", "description": null, "expected": "read"}, {"name": "list_deleted_items", "description": null, "expected": "read"}, {"name": "find_deleted", "description": null, "expected": "read"}, - {"name": "describe_purge_job", "description": null, "expected": "delete"}, + {"name": "describe_purge_job", "description": null, "expected": "read"}, {"name": "updateItem", "description": null, "expected": "update"}, {"name": "create-record", "description": null, "expected": "create"}, {"name": "foo", "description": "Deletes the file", "expected": "delete"}, @@ -24,5 +24,6 @@ {"name": "settings", "description": null, "expected": "unknown"}, {"name": "get_and_delete_item", "description": null, "expected": "delete"}, {"name": "fetchAndDelete", "description": null, "expected": "delete"}, - {"name": "read_then_remove", "description": null, "expected": "delete"} + {"name": "read_then_remove", "description": null, "expected": "delete"}, + {"name": "getDeleteStatus", "description": null, "expected": "read"} ] diff --git a/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts index 56a4de86959..487842317d5 100644 --- a/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts +++ b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts @@ -98,9 +98,14 @@ const tokenVariants = (token: string): string[] => (variant) => variant.length > 0, ); +const CONJUNCTION_TOKENS = new Set(["and", "then", "or", "n"]); + // Matches litellm/proxy/_experimental/mcp_server/tool_classification.py, fixture-pinned. const classifyTokens = (tokens: string[]): CrudOp => { - if (tokens.some((token) => DELETE_TOKENS.has(token))) return "delete"; + const hasBareDeleteVerb = tokens.some( + (token, index) => DELETE_TOKENS.has(token) && (index === 0 || CONJUNCTION_TOKENS.has(tokens[index - 1])), + ); + if (hasBareDeleteVerb) return "delete"; const variants = new Set(tokens.flatMap((token) => tokenVariants(token))); if ([...variants].some((variant) => READ_TOKENS.has(variant))) return "read"; if ([...variants].some((variant) => DELETE_TOKENS.has(variant))) return "delete";