litellm/tests/local_testing/test_azure_anthropic_sync_post.py
Sameer Kankute 02bce7b393
fix(mcp): honor server_id for REST tool calls with shared upstream URLs (#30184)
* fix(mcp): honor server_id for REST tool calls with shared upstream URLs

When multiple MCP server entries point at the same backend URL and tool
name, REST /mcp-rest/tools/call now routes and applies auth from the
requested server_id instead of the global unprefixed tool-name mapping.

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

* fix(mcp): classify prefixed REST tool names against full registry

Use all registered MCP server prefixes for prefix detection so
unauthorized prefixed names still trigger tool_server_mismatch, and
reject ambiguous hyphenated REST tool names with server_id.

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

* test(mcp): cover server_id fallback for unresolved prefixed REST tool names

execute_mcp_tool left the prefix-retry and requested-server fallback
branches uncovered, dropping diff coverage below the project target.
Add a regression test for a REST call that passes server_id with a
prefixed tool name that resolves to no managed tool; it must still
dispatch to the server identified by server_id rather than the server
named by the prefix.

* test(mcp): scope global tool-name mapping mutation with patch.dict

* test(mcp): cover server_id guard on prefix-retry tool resolution

The prefix-retry branch in execute_mcp_tool re-prefixes the tool name with
the requested server's known prefixes when the bare lookup misses. The
candidate-found path that assigns mcp_server from that lookup stayed
uncovered, so codecov patch coverage remained below the diff target.

Add a regression test where the re-prefixed lookup resolves a server whose
server_id differs from the requested server_id; the tool_server_mismatch
403 guard must still fire instead of being silently bypassed.

* test(mcp): assert requested server credentials injected on cross-server REST routing

* perf(mcp): scan registry prefixes only when server_id is supplied

* fix(mcp): allow hyphenated upstream tool names when REST server_id is authoritative

* perf(mcp): skip registry prefix scan for separator-free REST tool names

* test(http_handler): drop httpbin dependence from per-request timeout test

The per-request timeout test posted to https://httpbin.org/delay/10 and asserted
a Timeout was raised. httpbin's free /delay endpoint intermittently returns 503
even when the /get reachability guard succeeds, so local_testing_part1 flaked on
that 503 instead of the expected timeout (failed identically across an initial run
and a rerun-from-failed). Serve the slow response from a local ThreadingHTTPServer
so the timeout fires deterministically with no third-party network dependence.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-12 07:25:53 -07:00

67 lines
2 KiB
Python

"""
``_get_httpx_client`` + ``HTTPHandler.post`` (same pattern as Azure Anthropic sync path:
``_get_httpx_client(params={"timeout": ...})`` then ``post(..., timeout=...)``).
A local server stalls longer than the per-request ``timeout`` but well under the client
default, so the handler must raise :class:`~litellm.exceptions.Timeout` from the per-request
override rather than completing under the (much larger) client default.
Lives under ``local_testing`` (not ``make test-unit``).
"""
import json
import os
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import pytest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from litellm.exceptions import Timeout as LitellmTimeout
from litellm.llms.custom_httpx.http_handler import (
MaskedHTTPStatusError,
_get_httpx_client,
)
_SERVER_DELAY_S = 5
_PER_REQUEST_TIMEOUT_S = 1.0
_CLIENT_DEFAULT_TIMEOUT_S = 60.0
class _SlowHandler(BaseHTTPRequestHandler):
def do_POST(self):
time.sleep(_SERVER_DELAY_S)
try:
self.send_response(200)
self.end_headers()
self.wfile.write(b"{}")
except OSError:
pass
def log_message(self, *args):
pass
def test_post_delay_exceeds_per_request_timeout_raises():
server = ThreadingHTTPServer(("127.0.0.1", 0), _SlowHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()
host, port = server.server_address
handler = _get_httpx_client(params={"timeout": _CLIENT_DEFAULT_TIMEOUT_S})
try:
with pytest.raises(LitellmTimeout):
handler.post(
f"http://{host}:{port}/delay",
headers={"content-type": "application/json"},
data=json.dumps({"model": "claude", "messages": []}),
timeout=_PER_REQUEST_TIMEOUT_S,
)
except MaskedHTTPStatusError as e:
pytest.skip(f"httpbin.org unavailable: {e}")
finally:
handler.close()
server.shutdown()
server.server_close()