mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(mcp): restrict list JWTs to mcp:tools/list and default REST arguments to {}
- List-only JWTs (call_type=list_mcp_tools) no longer carry the broad
mcp:tools/call scope. _build_scope() now emits only mcp:tools/list
when no tool name is provided, mirroring the existing least-privilege
rule that tool-call JWTs omit mcp:tools/list.
- REST /tools/call now defaults a missing 'arguments' field to {} so
execute_mcp_tool() and downstream **arguments / .keys() calls don't
receive None and crash with TypeError/AttributeError.
Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
parent
93db9e0dad
commit
5499662926
38 changed files with 13 additions and 7 deletions
|
|
@ -802,7 +802,7 @@ if MCP_AVAILABLE:
|
|||
},
|
||||
)
|
||||
|
||||
tool_arguments = data.get("arguments")
|
||||
tool_arguments = data.get("arguments") or {}
|
||||
|
||||
proxy_base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data)
|
||||
(
|
||||
|
|
|
|||
|
|
@ -612,10 +612,12 @@ class MCPJWTSigner(CustomGuardrail):
|
|||
When allowed_scopes is configured: join them verbatim.
|
||||
Otherwise auto-generate minimal, least-privilege scopes:
|
||||
- Tool call → mcp:tools/call mcp:tools/<name>:call
|
||||
- No tool → mcp:tools/call mcp:tools/list
|
||||
- No tool → mcp:tools/list
|
||||
|
||||
NOTE: tools/list is intentionally NOT granted on tool-call JWTs to
|
||||
prevent callers from enumerating tools they didn't ask to use.
|
||||
Conversely, tools/call is NOT granted on tools/list-only JWTs so an
|
||||
intercepted list token cannot be replayed to invoke tools.
|
||||
"""
|
||||
if self.allowed_scopes is not None:
|
||||
return " ".join(self.allowed_scopes)
|
||||
|
|
@ -626,7 +628,7 @@ class MCPJWTSigner(CustomGuardrail):
|
|||
if tool_name:
|
||||
scopes = ["mcp:tools/call", f"mcp:tools/{tool_name}:call"]
|
||||
else:
|
||||
scopes = ["mcp:tools/call", "mcp:tools/list"]
|
||||
scopes = ["mcp:tools/list"]
|
||||
return " ".join(scopes)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ def test_build_claims_scope_with_tool():
|
|||
|
||||
|
||||
def test_build_claims_scope_without_tool():
|
||||
"""_build_claims() includes mcp:tools/list when no specific tool is called."""
|
||||
"""_build_claims() emits only mcp:tools/list when no specific tool is called."""
|
||||
signer = _make_signer()
|
||||
user_dict = _make_user_api_key_dict()
|
||||
data: Dict[str, Any] = {}
|
||||
|
|
@ -227,10 +227,11 @@ def test_build_claims_scope_without_tool():
|
|||
claims = signer._build_claims(user_dict, data)
|
||||
|
||||
scopes = set(claims["scope"].split())
|
||||
assert "mcp:tools/call" in scopes
|
||||
assert "mcp:tools/list" in scopes
|
||||
# List-only JWTs must NOT carry mcp:tools/call — least-privilege
|
||||
assert "mcp:tools/call" not in scopes
|
||||
# No per-tool call scope when no tool name was given
|
||||
assert not any(s.endswith(":call") and s != "mcp:tools/call" for s in scopes)
|
||||
assert not any(s.endswith(":call") for s in scopes)
|
||||
|
||||
|
||||
def test_build_claims_act_fallback_to_litellm_proxy():
|
||||
|
|
@ -372,7 +373,10 @@ async def test_hook_signs_list_mcp_tools():
|
|||
assert result["extra_headers"]["Authorization"].startswith("Bearer ")
|
||||
token = result["extra_headers"]["Authorization"].removeprefix("Bearer ")
|
||||
decoded = _decode_unverified(token)
|
||||
assert "mcp:tools/list" in decoded["scope"]
|
||||
scopes = set(decoded["scope"].split())
|
||||
assert "mcp:tools/list" in scopes
|
||||
# List-only JWTs must NOT carry mcp:tools/call — least-privilege
|
||||
assert "mcp:tools/call" not in scopes
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue