Repository navigation
test(integration): add MCP gateway coverage wave 1 with a dedicated mcp shard and proxy coverage artifact - #42711
Merged
Merged
Conversation
Contributor
Author
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
Contributor
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
Author
Contributor
Author
…quirement Groups live as a GROUPS literal in run.py, the browser expectations move next to the browser tests, and the runner fails only on pytest failure, collection errors or a selected file that collects zero tests. The covers marker stays registered for the existing tests but is no longer checked. The mcp directory gets its own group Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… for the MCP modules The mcp shard sets it. The proxy and its peer start under coverage run in parallel mode, get SIGTERM after the tests so coverage flushes, and the combined text and HTML reports land in the suite results that CircleCI already stores as artifacts Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ises SIGTERM Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…P peer doubles Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ience and lifecycle coverage Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…initialize as a leaked call and satisfy the test-tree lint Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…r empty Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…enied Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration
Bot
force-pushed
the
litellm_mcp_integration_coverage
branch
from
September 23, 2026 14:37
35f6f3e to
951b9f9
Compare
yuneng-berri
approved these changes
Sep 23, 2026
This was referenced Sep 23, 2026
test(integration): regression tests for July provider translation, routing and streaming bugs
#42693
Merged
Merged
13 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
How it solves it:
mcpCircleCI shard with xdist and a proxy coverage artifactrun.py, browser list next to the browser tests, manifest gonepytest.skip("BUG: ...")with evidence belowUser Flow
Before: a maintainer merges an MCP change that silently breaks tool access and only learns about it from a customer report
extensionsintegration shard, which holds 11 MCP teststools/callwith a key that has no grant for that server and gets"isError": falseand the tool output backAfter: the same PR turns the
mcpshard red before mergemcpintegration shard with 229 teststest_mcp_access_matrix.py::test_subject_grant_lists_only_reachable_tools_and_denies_the_rest[...]withdenied call succeeded: {...}and the peer-observed request attachedcoverage.txtartifact to see which MCP module lines the suite reachedRelevant issues
Follow-up to #42687 (skipped nodes count as complete, shard cap dropped), which this PR builds on
Affected release
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
This PR changes no proxy behavior, so the proof is what the harness observes, measured against a real proxy pair (two workers on :4000 and :4001), real Postgres and Redis, and the peer doubles from
tests/integration/_support/. No provider APIs are involved: every MCP peer, LLM upstream and OAuth server is a local double at the protocol edge, which is the point of the suitePart A: node counts per shard, before and after the manifest removal
Collected with
pytest --collect-only -qover each group's directories at the merge base (5028f9e) and at the tipEvery non-MCP shard collects the same nodes as before.
mcpgrew from 11 to 229 nodes, so 218 nodes were addedPart A: MCP module coverage of the proxy, before and after
INTEGRATION_COVERAGE=1runs the proxy undercoverage run --parallel-modescoped to the MCP modules (tests/integration/mcp_coverage.toml), sends SIGTERM at the end so the data flushes, then combines and writescoverage.txtand an HTML report under the suite results, which CircleCI stores as an artifact. Themcpshard has it on. Measured locally by running the same command with the 11 existing MCP nodes at the base and all 229 at the tip (the MCP modules are byte-identical at both commits, this PR does not touchlitellm/)coverage.txttotalLine totals differ because coverage only lists modules the proxy imported during the run and the new tests import more of them
Part B: local run of the
mcpshard at the tipPart B: bug table, one row per
BUG:skipEach of these fails on the product, so the test skips with the symptom and the table carries the evidence. A guarded assertion sits right after every skip, so the test goes red again the moment the product starts behaving
test_mcp_management.py::test_duplicate_alias_is_rejected_so_tool_prefixes_cannot_collidePOST /v1/mcp/serverwithaliasequal to an existing server's alias201and a secondserver_id; both servers now expose<alias>-add400naming the duplicate aliastest_mcp_lifecycle.py::test_key_grant_added_by_key_update_is_visible_to_mcp_tool_listing_before_the_cache_ttlPOST /key/updateaddingobject_permission.mcp_servers: [<id>], thenPOST /mcptools/listwith that keytools: []for the full 60s key cache TTL; the second worker agrees; no invalidation is publishedtest_mcp_oauth_flows.py::test_token_exchange_without_a_subject_token_is_rejected_before_any_upstream_requestPOST /mcp-rest/tools/callon anoauth2_token_exchangeserver with no callerAuthorization500; peer and token endpoint untouched (fail-closed)401telling the caller a subject token is requiredtest_mcp_oauth_flows.py::test_delegated_auth_forwards_the_callers_bearer_untouched[rest]POST /mcp-rest/tools/callon anoauth_delegateserver withAuthorization: Bearer user-...tools/callwith noauthorizationheader;/mcp,/{server}/mcp,/mcp/and/mcp/sseforward it/mcp-resttootest_mcp_accounting_guardrails.py::test_pre_mcp_call_guardrail_blocks_before_the_peer_and_still_logs_spend[rest]POST /mcp-rest/tools/callwith an argument containing the content filter's blocked wordLiteLLM_SpendLogsrow hasmodel=""and no tool name inmcp_tool_call_metadata; the JSON-RPC entry points logMCP: <alias>-addtest_mcp_llm_endpoints.py::test_auto_approved_gateway_tool_is_listed_executed_once_and_fed_back[messages_bridge](and the othermessages_bridgecases)POST /v1/messagesagainst a non-Anthropic model withmcp_servers: [{server_url: "litellm_proxy", require_approval: "never"}]tool_resultblock, so the proxy re-runs the tool on every loop iteration until the capthe sum is 5Part B: mutation proof for invariants 1, 2, 7 and 8
Each mutation was applied to
litellm/in a scratch worktree only, the matching tests were run against that proxy, then the worktree was restored withgit checkout litellm/and the same tests passed again. Nothing underlitellm/is in this PR's diffoperations.py: disable the server-level authorization checktest_mcp_access_matrix.py48 failed, 42 passed; representative failuredenied call succeeded: {"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"3",...}],"isError":false}}operations.py: resolve the caller's allowed servers to the whole registrytest_mcp_access_matrix.py74 failed, 19 passed; listings show ungranted tools that then fail or succeed unexpectedly on callencrypt_value_helper: return the string unchangedtest_mcp_credentials.py -k encrypted_at_rest4 failed; all four auth modes (api_key,bearer_token,basic,authorization) found the secret inLiteLLM_MCPServerTablecost_calculator.py:calculate_mcp_tool_call_costreturns0.0when a logging object existstest_mcp_accounting_guardrails.py -k configured_costObtained: 0.0 / Expected: 0.5 ± 5.0e-07on theaddrow, same for the default cost rowAfter restoring the worktree:
test_mcp_access_matrix.py138 passed,test_mcp_credentials.pyand the accounting file all green in the full run aboveFix-history mapping: 368 integration-testable rows
Each row of
mcp-fix-history.mdmaps to the test node that would catch a regression of that fix, or tonot covered: <reason>. Rows are keyed by the fix commit's short sha. 260 rows map to a node, 108 are not covered, the reasons repeat because they share a cause: JWT auth, network origin policy, proxy log text, UI-only changes, or behavior the harness cannot observe without a real third partyoauth: 117 rows, 100 mapped, 17 not covered
test_mcp_oauth_flows.py::test_per_user_authorization_code_with_pkce_binds_the_token_to_the_authorizing_usertest_mcp_oauth_flows.py::test_dcr_bridge_relays_client_registration_and_advertises_gateway_endpointstest_oauth_configuration.py::test_partial_discovery_and_unrelated_edit_keep_actual_authorization_destinationtest_mcp_oauth_flows.py::test_client_credentials_token_is_minted_once_and_sent_as_bearertest_mcp_oauth_flows.py::test_delegated_auth_forwards_the_callers_bearer_untouchedtest_mcp_oauth_flows.py::test_token_exchange_swaps_the_callers_subject_token_and_never_forwards_ittest_mcp_credentials.py::test_static_credential_reaches_the_peer_in_its_mode_shape_and_is_encrypted_at_resttest_mcp_management.py::test_config_declared_server_behaves_like_database_server_but_is_read_onlytest_mcp_access_matrix.py::test_same_tool_name_on_two_servers_routes_by_prefixtest_mcp_oauth_flows.py::test_authorization_request_without_pkce_is_refused_before_reaching_the_authorization_serveraccess_control: 58 rows, 42 mapped, 16 not covered
test_mcp_access_matrix.py::test_subject_grant_lists_only_reachable_tools_and_denies_the_resttest_mcp_oauth_flows.py::test_dcr_bridge_relays_client_registration_and_advertises_gateway_endpointstest_mcp_management.py::test_access_group_membership_follows_editstest_mcp_access_matrix.py::test_key_without_any_grant_sees_no_scoped_servertest_mcp_transports.py::test_server_initiated_sampling_and_elicitation_surface_as_errors_not_successtest_mcp_lifecycle.py::test_health_intersects_route_restricted_key_grants_in_both_management_modestest_mcp_oauth_flows.py::test_token_exchange_swaps_the_callers_subject_token_and_never_forwards_ittest_mcp_transports.py::test_every_entry_point_lists_and_calls_every_peer_transporttest_mcp_access_matrix.py::test_missing_or_wrong_key_is_rejected_before_the_peertest_mcp_transports.py::test_rest_and_streamable_http_agree_on_tool_list_and_resulttest_mcp_llm_endpoints.py::test_gateway_tool_without_auto_approval_returns_the_call_to_the_caller_and_never_hits_the_peertest_mcp_credentials.py::test_caller_headers_for_other_servers_and_unknown_headers_never_reach_the_peertest_mcp_oauth_flows.py::test_delegated_auth_forwards_the_callers_bearer_untouchedtest_mcp_oauth_flows.py::test_per_user_authorization_code_with_pkce_binds_the_token_to_the_authorizing_usertest_mcp_management.py::test_secrets_never_appear_in_server_listing_or_detailcredentials_byok: 38 rows, 28 mapped, 10 not covered
test_mcp_credentials.py::test_static_credential_reaches_the_peer_in_its_mode_shape_and_is_encrypted_at_resttest_mcp_oauth_flows.py::test_client_credentials_token_is_minted_once_and_sent_as_bearertest_oauth_configuration.py::test_same_url_oauth_credentials_and_revocation_are_isolated_by_user_and_servertest_mcp_oauth_flows.py::test_delegated_auth_forwards_the_callers_bearer_untouchedtest_mcp_credentials.py::test_byok_server_uses_the_calling_users_stored_credential_and_fails_closed_without_onetest_mcp_transports.py::test_rest_and_streamable_http_agree_on_tool_list_and_resulttest_mcp_oauth_flows.py::test_dcr_bridge_relays_client_registration_and_advertises_gateway_endpointstest_mcp_management.py::test_config_declared_server_behaves_like_database_server_but_is_read_onlyguardrails: 29 rows, 10 mapped, 19 not covered
test_mcp_accounting_guardrails.py::test_pre_mcp_call_guardrail_blocks_before_the_peer_and_still_logs_spendtest_mcp_credentials.py::test_byok_server_uses_the_calling_users_stored_credential_and_fails_closed_without_onemanagement_crud: 29 rows, 17 mapped, 12 not covered
test_mcp_access_matrix.py::test_subject_grant_lists_only_reachable_tools_and_denies_the_resttest_mcp_management.py::test_config_declared_server_behaves_like_database_server_but_is_read_onlytest_oauth_configuration.py::test_partial_discovery_and_unrelated_edit_keep_actual_authorization_destinationtest_mcp_management.py::test_secrets_never_appear_in_server_listing_or_detailtest_mcp_credentials.py::test_static_credential_reaches_the_peer_in_its_mode_shape_and_is_encrypted_at_resttest_mcp_oauth_flows.py::test_delegated_auth_forwards_the_callers_bearer_untouchedtest_mcp_oauth_flows.py::test_dcr_bridge_relays_client_registration_and_advertises_gateway_endpointstest_mcp_management.py::test_edit_url_moves_calls_to_the_new_peer_without_touching_grantstest_mcp_lifecycle.py::test_saved_headers_reach_real_mcp_tool_and_survive_unrelated_editllm_endpoint_tools: 21 rows, 19 mapped, 2 not covered
test_mcp_llm_endpoints.py::test_auto_approved_gateway_tool_is_listed_executed_once_and_fed_backtest_mcp_access_matrix.py::test_subject_grant_lists_only_reachable_tools_and_denies_the_resttest_mcp_transports.py::test_every_entry_point_lists_and_calls_every_peer_transporttest_mcp_transports.py::test_server_initiated_sampling_and_elicitation_surface_as_errors_not_successtest_mcp_lifecycle.py::test_health_intersects_route_restricted_key_grants_in_both_management_modestest_mcp_llm_endpoints.py::test_server_scoped_gateway_url_exposes_only_that_servers_toolstest_mcp_transports.py::test_rest_and_streamable_http_agree_on_tool_list_and_resulttest_mcp_resilience.py::test_tool_error_surfaces_as_error_with_the_peer_message_and_never_as_successtest_mcp_access_matrix.py::test_same_tool_name_on_two_servers_routes_by_prefixtest_mcp_credentials.py::test_byok_server_uses_the_calling_users_stored_credential_and_fails_closed_without_onetest_mcp_lifecycle.py::test_same_url_server_grants_scope_discovery_and_direct_or_virtual_executiondiscovery_listing: 18 rows, 12 mapped, 6 not covered
test_oauth_configuration.py::test_partial_discovery_and_unrelated_edit_keep_actual_authorization_destinationtest_mcp_access_matrix.py::test_same_tool_name_on_two_servers_routes_by_prefixtest_mcp_resilience.py::test_unreachable_peer_errors_while_a_healthy_sibling_keeps_servingtest_mcp_accounting_guardrails.py::test_pre_mcp_call_guardrail_blocks_before_the_peer_and_still_logs_spendtest_mcp_credentials.py::test_byok_server_uses_the_calling_users_stored_credential_and_fails_closed_without_onetest_mcp_transports.py::test_rest_and_streamable_http_agree_on_tool_list_and_resulttest_mcp_access_matrix.py::test_subject_grant_lists_only_reachable_tools_and_denies_the_restprotocol_transport: 17 rows, 13 mapped, 4 not covered
test_mcp_transports.py::test_every_entry_point_lists_and_calls_every_peer_transporttest_mcp_transports.py::test_official_client_session_lists_and_calls_through_gatewaytest_mcp_resilience.py::test_slow_peer_beyond_configured_timeout_errors_and_does_not_hang_the_gatewaytest_mcp_access_matrix.py::test_same_tool_name_on_two_servers_routes_by_prefixtest_mcp_oauth_flows.py::test_delegated_auth_forwards_the_callers_bearer_untouchedtest_mcp_transports.py::test_progress_notifications_do_not_break_result_and_slow_tool_completestest_mcp_access_matrix.py::test_missing_or_wrong_key_is_rejected_before_the_peertest_mcp_resilience.py::test_tool_error_surfaces_as_error_with_the_peer_message_and_never_as_successhealth_resilience: 15 rows, 3 mapped, 12 not covered
test_mcp_lifecycle.py::test_health_intersects_route_restricted_key_grants_in_both_management_modestest_oauth_configuration.py::test_partial_discovery_and_unrelated_edit_keep_actual_authorization_destinationaccounting_logging: 14 rows, 7 mapped, 7 not covered
test_mcp_credentials.py::test_static_credential_reaches_the_peer_in_its_mode_shape_and_is_encrypted_at_resttest_mcp_accounting_guardrails.py::test_pre_mcp_call_guardrail_blocks_before_the_peer_and_still_logs_spendtest_mcp_oauth_flows.py::test_per_user_authorization_code_with_pkce_binds_the_token_to_the_authorizing_usertest_mcp_accounting_guardrails.py::test_each_tool_call_writes_one_spend_row_with_server_tool_and_configured_costtest_mcp_accounting_guardrails.py::test_key_spend_and_key_max_budget_count_mcp_tool_callsmulti_worker_cache: 12 rows, 9 mapped, 3 not covered
test_mcp_credentials.py::test_byok_server_uses_the_calling_users_stored_credential_and_fails_closed_without_onetest_mcp_oauth_flows.py::test_per_user_authorization_code_with_pkce_binds_the_token_to_the_authorizing_usertest_mcp_access_matrix.py::test_subject_grant_lists_only_reachable_tools_and_denies_the_resttest_mcp_management.py::test_edit_url_moves_calls_to_the_new_peer_without_touching_grantstest_mcp_lifecycle.py::test_health_intersects_route_restricted_key_grants_in_both_management_modestest_oauth_configuration.py::test_same_url_oauth_credentials_and_revocation_are_isolated_by_user_and_serverType
✅ Test
🚄 Infrastructure
Caveats (if any)
Medium
BUG:skip; the table above has the repromcpshard takes about 3 minutes with 4 xdist workers, the longest of the integration shardstest_mcp_lifecycle.pyruns a Hypothesis state machine capped at 5 examples of 6 steps to stay inside the HTTP budgetLow
run-ciis the authoritative Aftercontracts.jsondrops thecoversrequirement. Existing@pytest.mark.covers(...)markers stay registered and inertnot coveredrows are honest gaps for a later wave (JWT auth mode, origin allowlists, log assertions, UI)Final Attestation
Link to Devin session: https://app.devin.ai/sessions/8c808db0bc054c3c8a1700ff5a843c56
Open in Devin Desktop: https://app.devin.ai/desktop/session/8c808db0bc054c3c8a1700ff5a843c56?variant=devin