Repository navigation
feat(proxy): embed admin MCP behind enterprise license and opt-in #43881
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
89ce9dc
feat(proxy): embed admin MCP behind an opt-in flag
tin-berri 2142d5e
fix(proxy): preserve cleanup when admin MCP lifecycle fails
tin-berri c2efee7
fix(proxy): drain admin MCP and reuse nested admission slots
tin-berri 014c3e0
fix(proxy): centralize admin MCP shutdown and scope admission reuse
tin-berri 82489ac
feat(proxy): require base enterprise license for hosted admin MCP
tin-berri cd5c3c3
chore: sync main for upstream CI repairs
tin-berri 6334cae
test(proxy): use a real request for batch metadata validation
tin-berri 2c42ab9
fix(ci): validate admin MCP dependencies and restore coverage uploads
tin-berri 390ea37
test(tracing): verify current native storage contract
tin-berri 8b575b8
fix(deps): patch GitPython and Tornado advisories
tin-berri 5684f42
fix(admin-mcp): return 403 for userless keys; merge main
tin-berri b8ff2ae
Merge remote-tracking branch 'origin/main' into litellm_embedded_admi…
tin-berri bce2c79
fix(admin-mcp): pin merged connector, accept on/off flag spellings; m…
tin-berri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import os | ||
| from collections.abc import AsyncGenerator | ||
| from contextlib import asynccontextmanager | ||
| from contextvars import ContextVar | ||
| from typing import Final | ||
| from urllib.parse import urlsplit | ||
|
|
||
| from fastapi import FastAPI | ||
| from starlette.datastructures import Headers | ||
| from starlette.requests import Request | ||
| from starlette.routing import Mount | ||
| from starlette.types import ASGIApp, Receive, Scope, Send | ||
|
|
||
| from litellm.proxy.middleware.admission_control_middleware import ADMISSION_LEASE_SCOPE_KEY | ||
|
|
||
| _REQUEST_HEADERS: Final = frozenset( | ||
| { | ||
| b"authorization", | ||
| b"cookie", | ||
| b"content-length", | ||
| b"content-type", | ||
| b"transfer-encoding", | ||
| b"connection", | ||
| b"accept", | ||
| b"accept-encoding", | ||
| b"mcp-protocol-version", | ||
| b"mcp-session-id", | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def _require_enterprise_license() -> None: | ||
| from litellm.proxy.utils import require_enterprise_license | ||
|
|
||
| require_enterprise_license("Hosted admin MCP") | ||
|
|
||
|
|
||
| class _CallerContext: | ||
| def __init__(self, app: ASGIApp, caller: ContextVar[Request]) -> None: | ||
| self.app = app | ||
| self.caller = caller | ||
|
|
||
| async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | ||
| if scope["type"] != "http": | ||
| await self.app(scope, receive, send) | ||
| return | ||
| _require_enterprise_license() | ||
| token: Final = self.caller.set(Request(scope)) | ||
| try: | ||
| await self.app(scope, receive, send) | ||
| finally: | ||
| self.caller.reset(token) | ||
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def admin_mcp_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: | ||
| enabled: Final = os.environ.get("LITELLM_ENABLE_ADMIN_MCP", "false").strip().lower() | ||
| if enabled in ("false", "0", "off", "no", ""): | ||
| yield | ||
| return | ||
| if enabled not in ("true", "1", "on", "yes"): | ||
| raise ValueError("LITELLM_ENABLE_ADMIN_MCP must be true or false") | ||
| _require_enterprise_license() | ||
|
|
||
| try: | ||
| import httpx2 | ||
| from litellm_admin_mcp.config import ( # pyright: ignore[reportMissingTypeStubs] # upstream has no py.typed marker | ||
| Config, | ||
| env_bool, | ||
| ) | ||
| from litellm_admin_mcp.gateway import ( # pyright: ignore[reportMissingTypeStubs] # upstream has no py.typed marker | ||
| Gateway, | ||
| ) | ||
| from litellm_admin_mcp.server import ( # pyright: ignore[reportMissingTypeStubs] # upstream has no py.typed marker | ||
| create_http_app, | ||
| ) | ||
| except ImportError as exc: | ||
| raise RuntimeError( | ||
| "Admin MCP requires Python 3.12+ and the admin-mcp dependency group. " | ||
| "Use a LiteLLM image that bundles it, or run uv sync --extra proxy --group admin-mcp." | ||
| ) from exc | ||
|
|
||
| configured_url: Final = os.environ.get("LITELLM_MCP_PUBLIC_URL") or os.environ.get("PROXY_BASE_URL", "") | ||
| public_url: Final = urlsplit(configured_url) | ||
| config: Final = Config( | ||
| base_url="http://localhost", | ||
| public_url=f"{public_url.scheme}://{public_url.netloc}" if public_url.netloc else configured_url, | ||
| read_only=env_bool("LITELLM_ADMIN_READ_ONLY"), | ||
| allowed_tools=frozenset( | ||
| name.strip() for name in os.environ.get("LITELLM_ADMIN_TOOLS", "").split(",") if name.strip() | ||
| ), | ||
| response_view=os.environ.get("LITELLM_ADMIN_RESPONSE_VIEW", "full").strip(), | ||
| schema_mode=os.environ.get("LITELLM_ADMIN_SCHEMA_MODE", "full").strip(), | ||
| ) | ||
| caller: Final[ContextVar[Request]] = ContextVar("admin_mcp_caller") | ||
|
|
||
| async def management_api(scope: Scope, receive: Receive, send: Send) -> None: | ||
| request: Final = caller.get() | ||
| caller_headers: Final = tuple(pair for pair in request.headers.raw if pair[0] not in _REQUEST_HEADERS) | ||
| api_headers: Final = tuple(pair for pair in Headers(scope=scope).raw if pair[0] in _REQUEST_HEADERS) | ||
| headers: Final = list(caller_headers + api_headers) # mutable-ok: ASGI middleware modifies headers | ||
| gateway_scope: Final[Scope] = { | ||
| **scope, | ||
| "client": request.client, | ||
| "scheme": request.url.scheme, | ||
| "headers": headers, | ||
| ADMISSION_LEASE_SCOPE_KEY: request.scope.get(ADMISSION_LEASE_SCOPE_KEY), | ||
| } | ||
| await app(gateway_scope, receive, send) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| async with httpx2.AsyncClient(transport=httpx2.ASGITransport(app=management_api)) as client: | ||
| admin_app: Final = create_http_app(Gateway(config, client)) | ||
| route: Final = Mount("/admin", app=_CallerContext(admin_app, caller), name="admin_mcp") | ||
| async with admin_app.router.lifespan_context(admin_app): | ||
| app.router.routes.insert(0, route) | ||
| try: | ||
| yield | ||
| finally: | ||
| app.router.routes.remove(route) | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.