Skip to content

feat(mcp): graft v2 resolver onto _create_mcp_client (none + api_key static family) - #31058

Merged
tin-berri merged 9 commits into
litellm_internal_stagingfrom
litellm_mcp_v2_bridge
Jun 24, 2026
Merged

feat(mcp): graft v2 resolver onto _create_mcp_client (none + api_key static family)#31058
tin-berri merged 9 commits into
litellm_internal_stagingfrom
litellm_mcp_v2_bridge

Conversation

@tin-berri

@tin-berri tin-berri commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

PR4 of the MCP v2 outbound-credential migration (the lean "Mini PR" track). Stacked on PR #31056 (base litellm_mcp_v2_resolver_skeleton) so this diff shows only the bridge, the first two live arms, and the graft that puts them on the request path. This lands the first live modes end to end: it builds the bridge, fills the none and shared-key api_key arms, and grafts the resolver into _create_mcp_client so those modes resolve through v2 while every other mode defers to v1 unchanged

Linear ticket

N/A (groundwork for MCP V2)

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review and received a Confidence Score of at least 4/5

Screenshots / Proof of Fix

The graft is live (no flag), so it is curl-able on a running proxy through the REST surface, which routes through _create_mcp_client. Real upstream MCP servers were configured (through the UI, plus one in config.yaml for the authorization mode the UI does not surface), one per migrated arm plus two oauth2 servers that must defer to v1:

  • dw -> https://mcp.deepwiki.com/mcp, auth_type: none (the none / NoOpAuth arm)
  • github -> https://api.githubcopilot.com/mcp/, auth_type: bearer_token (static api_key family; StaticHeaderAuth writing Authorization: Bearer <token>)
  • github_authz -> same upstream, auth_type: authorization (static api_key family; StaticHeaderAuth writing the token verbatim, no prefix)
  • linear, slack -> auth_type: oauth2 (not migrated; defer to v1 unchanged)

none arm (NoOpAuth) against a real no-auth upstream:

$ curl -s "http://localhost:4001/mcp-rest/tools/list?server_id=9c016dfd-8712-477e-8d1c-9de5aa5bd029" \
    -H "x-litellm-api-key: sk-1234" | jq '{tools:(.tools|length), names:[.tools[].name], error}'
{
  "tools": 3,
  "names": ["read_wiki_structure", "read_wiki_contents", "ask_question"],
  "error": null
}

static bearer arm (StaticHeaderAuth) against a real authenticated upstream:

$ curl -s "http://localhost:4001/mcp-rest/tools/list?server_id=aea4754a-f0c1-4349-a3e9-c1a7d84568e5" \
    -H "x-litellm-api-key: sk-1234" | jq '{tools:(.tools|length), error}'
{
  "tools": 33,
  "error": null
}

authorization arm (StaticHeaderAuth writing the value verbatim, no prefix) against the same upstream, declared in config.yaml since the UI does not expose this mode:

mcp_servers:
  github_authz:
    url: https://api.githubcopilot.com/mcp/
    transport: http
    auth_type: authorization
    authentication_token: os.environ/GITHUB_MCP_PAT
$ curl -s "http://localhost:4001/mcp-rest/tools/list?server_id=a4a2e24fe8923e388c6d6457cc150070" \
    -H "x-litellm-api-key: sk-1234" | jq '{tools:(.tools|length), error}'
{
  "tools": 44,
  "error": null
}

The authenticated GitHub calls returning their full tool catalogs are the load-bearing proof: the resolver built the StaticHeaderAuth and attached the credential (Bearer-prefixed for bearer_token, verbatim for authorization), and a real upstream accepted both. A broken graft would surface the upstream 401 as an empty list with an error. The token scheme (Authorization: token <PAT>) was also exercised and correctly emitted; GitHub rejects that scheme with a 400, which confirms the resolver sends exactly what the mode specifies rather than silently falling back.

Note on why this is functional rather than a header byte-diff: on v1 a static credential is built into the request headers dict; on v2 it rides as an httpx.Auth that writes the header at send time, so it does not appear in the --detailed_debug "litellm headers" line. The upstream accepting the call is the observable proof the right credential went out.

Type

🆕 New Feature

Changes

This wires the first two live credential modes onto v1's request path through a typed resolver: none and the shared-key api_key static-header family.

resolver.py fills in two of the seven arms. none returns a NoOpAuth; api_key reads the shared key straight from its config and returns a StaticHeaderAuth with the configured header name and prefix. The api_key BYOK source and the other five arms stay not_implemented, so an unbuilt mode still fails closed with a typed error. These arms read entirely from the config, so the provider still needs no injected collaborators.

adapter.py is the v1 to v2 edge. to_subject maps v1's principal onto the resolver's Subject. to_server_spec maps a v1 server onto a ServerSpec for a migrated mode and returns None for every other mode so the caller defers to v1; here it maps only none and the static-header family (api_key on X-API-Key, and bearer_token / token / authorization / basic on Authorization with their scheme prefix, basic base64-encoded), all shared-key, and defers BYOK, OAuth, token-exchange, client-credentials, and SigV4. raise_public maps a CredError onto the proxy's public HTTP contract and is the one edge allowed to raise. adapter.py imports v1 and is deliberately kept out of the package __init__, so the resolver core (resolver.py / types.py) stays free of v1 imports.

MCPClient gains an optional resolved_auth that the client factory attaches to its auth= slot, taking precedence over the SigV4 aws_auth. The graft in _create_mcp_client's HTTP/SSE branch decides per mode via to_server_spec: a migrated mode resolves through an injected UpstreamCredentialProvider and feeds the resulting httpx.Auth into resolved_auth, mapping a resolver error onto the public contract via raise_public; every other mode returns None and falls through to the unchanged v1 construction. The provider is injected into the manager at construction (default-constructed for production, a fake in tests), so the resolver is exercised through real DI rather than monkeypatching. resolve_mcp_auth now runs only when the mode defers, so a migrated server skips v1's token-exchange and M2M token fetches.

stdio is untouched. auth_type / auth_value never reach the upstream on the stdio path (_get_auth_headers is HTTP/SSE only), and an httpx.Auth is meaningless to a subprocess, so a stdio server with a migrated auth_type still defers to v1. No v1 code is deleted in this PR; resolve_mcp_auth's static return still backs stdio and the not-yet-migrated modes until later PRs in the sequence retire it.

Tests cover the two live arms (including the emitted header per scheme), the full to_server_spec mapping and defer table, to_subject, raise_public's status mapping, the MCPClient auth-slot precedence, and the graft itself: migrated HTTP modes attach resolved_auth, deferred modes and a missing static token fall back to v1's auth_value, a stdio server with a migrated auth_type stays on v1, and a resolver error maps to a 401 through the injected provider


Note

Medium Risk
Changes live upstream authentication for common static MCP server configs on every HTTP/SSE client build, though unmigrated modes and overrides are explicitly deferred to preserve v1 behavior.

Overview
Wires the v2 UpstreamCredentialProvider into _create_mcp_client for HTTP/SSE transports so none and shared static-key modes (api_key, bearer/token/authorization/basic) resolve to httpx.Auth (NoOpAuth / StaticHeaderAuth) instead of v1’s resolve_mcp_auth + header dict. OAuth, SigV4, BYOK, passthrough, missing tokens, mcp_auth_header overrides, and stdio still defer to v1 unchanged.

Adds adapter.py (to_server_spec, to_subject, raise_public) as the v1↔v2 bridge and MCPClient.resolved_auth, which the httpx factory prefers over SigV4 aws_auth. Resolver none and shared-key api_key arms are implemented; other modes remain not_implemented. Inbound Authorization in extra_headers skips attaching resolved auth so hooks/caller headers keep winning.

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

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.66667% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...imental/mcp_server/outbound_credentials/adapter.py 96.49% 2 Missing ⚠️
...mental/mcp_server/outbound_credentials/resolver.py 91.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR grafts the v2 credential resolver onto _create_mcp_client's HTTP/SSE branch, making none and the shared-key api_key static-header family (bearer, token, authorization, basic, api_key) the first live modes end-to-end. Every other mode (OAuth grants, SigV4, BYOK, passthrough) returns None from to_server_spec and falls through to v1 unchanged.

  • adapter.py (new) is the v1↔v2 bridge: to_server_spec maps migrated modes and defers everything else, to_subject maps the inbound principal, and raise_public is the sole edge that converts a CredError into an HTTPException. The match is exhaustive with an assert_never tail, and the BYOK early-return guard correctly defers regardless of auth_type.
  • resolver.py fills in the none arm (→ NoOpAuth) and the api_key shared-key arm (→ StaticHeaderAuth); all remaining arms stay as typed not_implemented stubs that fail closed.
  • test_resolver.py restores the test_every_auth_spec_kind_is_exercised completeness guard (live_kinds | stubbed_kinds == set(AuthSpecKind)), ensuring a new AuthSpecKind that ships without a test fails the suite immediately.

Confidence Score: 5/5

Safe to merge — the live modes are parity-verified against real upstreams, all deferred modes fall through to v1 unchanged, and the completeness guard is restored.

The graft is narrow and well-tested: migrated modes resolve through the injected provider and are covered end-to-end; deferred modes, stdio, and the per-request-override path each have dedicated tests that confirm v1 fallback is intact. The one defensive gap (resolved_auth potentially unbound if a non-conforming provider returns an unexpected type) cannot be triggered by any current production or test code path, as every existing provider always returns Ok or Error.

mcp_server_manager.py — the resolved_auth variable could be unbound if a custom UpstreamCredentialProvider returns something other than Ok or Error.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/outbound_credentials/adapter.py New v1↔v2 bridge: exhaustive match over MCPAuth with assert_never tail, correct base64 encoding for Basic auth, early BYOK/passthrough deferral guards, and raise_public mapping CredError tags to HTTP statuses.
litellm/proxy/_experimental/mcp_server/outbound_credentials/resolver.py Fills none (→ NoOpAuth) and api_key shared-key (→ StaticHeaderAuth) arms; Byok and every other arm remain not_implemented stubs with correct assert_never tail.
litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Grafts v2 resolver onto HTTP/SSE branch of _create_mcp_client; resolved_auth variable is only assigned inside case Ok, leaving it potentially unbound if a non-conforming provider returns an unexpected value.
litellm/experimental_mcp_client/client.py Adds resolved_auth parameter; fallback chain resolved_auth → aws_auth is correct, and _get_auth_headers() (v1 path) is unchanged.
tests/test_litellm/proxy/_experimental/mcp_server/outbound_credentials/test_resolver.py Restored test_every_auth_spec_kind_is_exercised completeness guard: live_kinds
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py Comprehensive graft tests: migrated modes attach resolved_auth, deferred modes fall back to v1 auth_value, stdio stays on v1, resolver error maps to 401, per-request override and extra-header conflict are both covered.
tests/test_litellm/proxy/_experimental/mcp_server/outbound_credentials/test_adapter.py New adapter tests cover all migrated modes, the full defer table (including BYOK across all auth_types), to_subject mapping, and raise_public status codes for all six CredError tags.
tests/mcp_tests/test_mcp_auth_priority.py Updated to verify the auth header via auth_flow (v2 path) rather than _get_auth_headers(); correctly checks the emitted Authorization header for the bearer_token mode.
tests/test_litellm/experimental_mcp_client/test_mcp_client.py New TestMCPClientResolvedAuth class covers the resolved_auth→aws_auth precedence and the fallback-to-aws_auth case.
litellm/proxy/_experimental/mcp_server/outbound_credentials/init.py Docstring updated to reflect that none and api_key modes are now live on the request path; public exports unchanged.
tests/mcp_tests/test_mcp_server.py Replaces MagicMock() with a real UserAPIKeyAuth() instance for list_tools, fixing an attribute-access issue exposed by the v2 adapter's to_subject call.

Reviews (7): Last reviewed commit: "test(mcp): restore the AuthSpecKind comp..." | Re-trigger Greptile

Comment thread litellm/proxy/_experimental/mcp_server/outbound_credentials/adapter.py Outdated
@tin-berri
tin-berri force-pushed the litellm_mcp_v2_resolver_skeleton branch from 6444639 to 39a8799 Compare June 23, 2026 16:00
Base automatically changed from litellm_mcp_v2_resolver_skeleton to litellm_internal_staging June 23, 2026 16:11
@tin-berri
tin-berri requested a review from a team June 23, 2026 16:11
@tin-berri
tin-berri force-pushed the litellm_mcp_v2_bridge branch from e57309b to 4a56c5c Compare June 23, 2026 16:23
@tin-berri

Copy link
Copy Markdown
Contributor Author

Addressed: removed should_defer entirely rather than memoizing. The double computation came from should_defer(server) re-invoking to_server_spec(server) and discarding the spec. to_server_spec already models defer as a value (it returns None), so the graft calls it once and branches on None (spec = to_server_spec(server); if spec is None: <v1> else: <use spec>); there is no second call to double, and no redundant predicate to keep in sync. Also rebased onto the latest litellm_internal_staging now that PRs 1-3 merged. @greptileai please re-review

@tin-berri
tin-berri force-pushed the litellm_mcp_v2_bridge branch 4 times, most recently from 4921883 to d166fa2 Compare June 23, 2026 19:07
@tin-berri

Copy link
Copy Markdown
Contributor Author

Updated since the last review: removed the redundant should_defer (the graft branches on to_server_spec(...) is None, one call); made to_server_spec an exhaustive match over auth_type with an assert_never tail so a new MCPAuth mode fails the type gate until it is explicitly mapped or deferred; collapsed the three spec helpers into one; and hoisted the BYOK defer to a single top-level guard so a BYOK server defers for any auth_type (a stray static token can no longer mis-route it to a shared-key spec), with a regression test across schemes. Also rebased onto the latest litellm_internal_staging now that PRs 1-3 merged. @greptileai please re-review

@tin-berri tin-berri changed the title feat(mcp): add v1 bridge + none/api_key resolver arms (unwired) feat(mcp): graft v2 resolver onto _create_mcp_client (none + api_key static family) Jun 23, 2026
Comment thread litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
@veria-ai

veria-ai Bot commented Jun 23, 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: 1 · PR risk: 0/10

@tin-berri
tin-berri force-pushed the litellm_mcp_v2_bridge branch from b9200be to ed4c55d Compare June 24, 2026 02:27
@tin-berri

Copy link
Copy Markdown
Contributor Author

Note: deleting the v1 static-credential code is gated by the override-on-static residual, not just per-mode migration

This came up reviewing what PR4 can delete. Recording it so the final v1 retirement isn't a surprise.

PR4 deletes zero v1 code, by design. The graft routes the none + shared-key static modes to the v2 resolver, but the static modes' v1 handling is shared, so nothing is exclusively dead:

  • resolve_mcp_auth's static return (return server.authentication_token) is the catch-all for stdio plus every deferred mode (oauth2 etc. fall through to it).
  • MCPClient._get_auth_headers()'s per-scheme branches (bearer_token / basic / api_key / authorization / token) are dead for the migrated v2 path (it sets resolved_auth, not auth_value), but still reached by two deferred paths: (a) a static-scheme server with a per-request override, and (b) BYOK servers (which use the api_key scheme).

The two precedence facets, and why they differ:

  • Facet B (an inbound Authorization already present, e.g. the MCP JWT-signer guardrail injecting it via extra_headers): handled on the v2 path via apply-if-absent. The graft skips resolved_auth when its header is already set, so the inbound header wins without deferring. This keeps hooks on v2 with no v1 fallback. It deliberately diverges from the egress design (which defers hook headers to v1); doing it in v2 is what makes the lean track's eventual full v1 deletion possible.
  • Facet A (the per-request override, mcp_auth_header): defers to v1, which honors the override. The override input is client-supplied (x-mcp-{server}-{header}, or the deprecated x-mcp-auth), so it is a permanent input, not merely written by the per-user fetch sites.

When the static v1 code becomes deletable. The _get_auth_headers static branches, resolve_mcp_auth's static return, and the override pre-check come out together, and only once both hold:

  1. BYOK has migrated (removes the api_key-branch reliance from the deferred BYOK path), and
  2. the override-on-a-static-server case is settled with an explicit decision: either feed the inbound override into the resolver arm so it wins on v2 (matching v1), or decide static servers ignore a client-supplied override (a documented behavior change).

Point 2 is the key correction: finishing the per-mode PRs does not auto-delete the override pre-check, because a client can send x-mcp-{static-server}-auth regardless of mode. It is a deliberate step at the final v1 retirement.

Unchanged from the plan: per-mode v1 branches (token_exchange, client_credentials, etc.) still delete incrementally in their own PR. stdio's auth_value is dead (never rendered into a header), so retiring it is a "stop calling resolve_mcp_auth" cleanup, not a migration. OpenAPI-spec servers (_register_openapi_tools) are a separate code path, out of scope for this migration.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai please re-review the latest commit. Since the last review this now covers the graft into _create_mcp_client plus the credential-isolation handling: a per-request mcp_auth_header override defers to v1, and an inbound Authorization already in extra_headers wins on the v2 path without deferring (the resolved auth is skipped when its header is already present)


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai please re-review the latest commit.

Fixed the stale module docstring in adapter.py flagged in the last review: it still said the bridge was unwired, but _create_mcp_client now resolves migrated modes through to_server_spec / to_subject / raise_public on the HTTP/SSE path, so the docstring now says that.

On the other nit (the resolved_auth read after the match in _create_mcp_client reading as possibly-unbound), that one is a false positive, so I left the block as-is. resolve_credentials returns a Result, which is Ok | Error; the Error arm calls raise_public, which is typed NoReturn, so the only arm that falls through to the read is Ok, where resolved_auth is assigned. basedpyright treats the match as exhaustive over the two-variant union and reports no possibly-unbound error there, which is why CI's type gate stays green


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai please re-review the latest commit. Updated the outbound_credentials/__init__.py package docstring to match the adapter one: it still said nothing here was wired onto a live request path, but this PR's graft makes the none and shared-key api_key modes live through _create_mcp_client, so it now says that and notes the rest still defer to v1


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai please re-review the latest commit. Restored the AuthSpecKind completeness guard in test_resolver.py that the test restructure dropped: it asserts the two live arms plus the _STUBBED parametrization together cover every AuthSpecKind, so a new mode that ships without a resolver test fails the suite. That closes the gap the type-level assert_never cannot see (the type gate forces a new mode to have an arm, but not a test)


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run


Generated by Claude Code

@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 7c11148. Configure here.

mateo-berri
mateo-berri previously approved these changes Jun 24, 2026

@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 dismissed their stale review June 24, 2026 14:11

One comment

@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.

One comment but otherwise lgtm! Great stuff 💯

@tin-berri
tin-berri requested a review from mateo-berri June 24, 2026 17:24
PR4a of the MCP v2 outbound-credential migration, stacked on the resolver skeleton.
Builds the bridge for the first live modes without wiring it onto the request path:

- resolver.py: the none arm (NoOpAuth) and the api_key shared-key arm (StaticHeaderAuth
  from the config); the BYOK source and the other five arms stay not_implemented.
- adapter.py: the v1 <-> v2 edge (to_subject, to_server_spec, raise_public, should_defer).
  to_server_spec maps only none + the static-header family and returns None to defer every
  other mode to v1. Imports v1, kept out of the package __init__ so the resolver core stays
  v1-free.
- MCPClient gains an optional resolved_auth that feeds the factory's auth= slot, taking
  precedence over the SigV4 aws_auth; default None keeps current behavior.

Nothing calls these from _create_mcp_client yet, so production behavior is unchanged; the
graft lands in PR4b. Unit tests cover the two arms, the full mapping table, and the auth
plumbing.
Wire the none + api_key static-family resolver arms from PR4a onto v1's
live request path. In _create_mcp_client's HTTP/SSE branch, to_server_spec
decides per mode: a migrated mode resolves through the injected
UpstreamCredentialProvider and feeds the resulting httpx.Auth into the new
resolved_auth slot; every other mode returns None and falls through to the
unchanged v1 construction. resolve_mcp_auth now runs only when the mode
defers, so a migrated server skips the v1 token-exchange / M2M I/O.

stdio is untouched: auth_type/auth_value never reach the upstream on the
stdio path (_get_auth_headers is HTTP/SSE only), so there is nothing to
graft there. No v1 code is deleted yet; resolve_mcp_auth's static return
still backs stdio and the not-yet-migrated modes until later PRs retire it.
Regression tests for the PR4 graft. Migrated HTTP modes resolve through the
provider into resolved_auth: none -> NoOpAuth, and the static api_key family
emits the right header per scheme (X-API-Key, Bearer, token, raw authorization,
base64 basic). Deferred modes (oauth2) and a missing static token fall back to
v1's auth_value. A stdio server with a migrated auth_type still defers to v1,
since httpx.Auth never reaches the subprocess. A resolver Error is mapped to the
public HTTP contract (401) via an injected provider, exercising the DI seam.
The graft attaches the resolved static credential as an httpx.Auth, whose auth
flow writes its header after extra_headers. That silently overrode an inbound
Authorization: a per-request mcp_auth_header override, or a header supplied via a
guardrail hook / static_headers / forwarded caller header. v1 lets those win, so
the graft had inverted the credential precedence for the migrated static modes.

Mirror the v2 egress credential-isolation invariant: defer the request to v1 when
mcp_auth_header is set, or when the header the resolved credential would write is
already present in extra_headers. none writes no header, so it never defers.
Regression tests for the precedence fix. A per-request mcp_auth_header override and an
Authorization already present in extra_headers (guardrail hook like the JWT signer,
static_headers, or a forwarded caller header) both defer a migrated static server to v1
so the inbound credential wins; none stays on v2 and does not clobber an inbound
Authorization since NoOpAuth writes nothing. The deferred cases assert resolved_auth is
None, which fails if the guard is removed.
…ring

For an Authorization already supplied via extra_headers (a guardrail hook such as the
JWT signer, static_headers, or a forwarded caller header), keep the request on the v2
path and skip resolved_auth rather than deferring to v1. The inbound header still wins
since nothing overwrites it, but hooks no longer pin a v1 fallback, which is what lets
resolve_mcp_auth be retired once the remaining modes migrate.

The mcp_auth_header per-request override still defers to v1, since that value becomes
the upstream credential rather than sitting in extra_headers; that defer falls away
once the per-user modes stop writing mcp_auth_header.
…e graft

adapter.py uses `from __future__ import annotations`, so the quoted "UserAPIKeyAuth" /
"MCPServer" annotations in to_subject/to_server_spec/_shared_key_spec were unnecessary
and pushed UP037 over the strict-rule budget; drop the quotes.

test_list_tools_only_returns_allowed_servers passed a MagicMock as user_api_key_auth.
The graft now builds a Subject from the principal, and the MagicMock's non-string
org_id/user_id fail Subject validation, so the listing came back empty. Use a real
UserAPIKeyAuth instead (MagicMock for an injected dependency was the anti-pattern here).
test_mcp_server_config_auth_value_header_used inspected _get_auth_headers(), but the
graft now carries the static credential on the client's httpx.Auth (resolved_auth) and
writes the header at send time, so that dict is empty. Assert the header the
StaticHeaderAuth emits onto the request instead. Both config keys (authentication_token,
auth_value) stay covered.
@tin-berri
tin-berri force-pushed the litellm_mcp_v2_bridge branch from 63cb433 to 50bd8b5 Compare June 24, 2026 20:25
The previous slack of 3 put the ceiling at baseline + slack = 4, so a newly
non-exhaustive match (for instance dropping an Error arm off a Result match)
could land without tripping the gate. Setting slack to 0 pins the ceiling at
the current baseline of 1, so any added non-exhaustive match now fails CI while
the one pre-existing violation in router.py stays within budget

@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!

@tin-berri
tin-berri merged commit bbef1b8 into litellm_internal_staging Jun 24, 2026
123 checks passed
@tin-berri
tin-berri deleted the litellm_mcp_v2_bridge branch June 24, 2026 21:53
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.

2 participants