From ba2069931235ceb9fcd349f52e4c6a2562a9db82 Mon Sep 17 00:00:00 2001 From: shivam Date: Fri, 20 Mar 2026 18:45:37 -0700 Subject: [PATCH] added support for metadata --- .circleci/config.yml | 36 +++++------- .circleci/requirements.txt | 2 +- .../proxy/_experimental/mcp_server/server.py | 47 +++++++++------- pyproject.toml | 2 +- requirements.txt | 2 +- .../mcp_server/test_mcp_server.py | 55 +++++++++++++++++-- 6 files changed, 96 insertions(+), 48 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7f410baf8fd..1b6dbfd5367 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,24 +33,17 @@ commands: - run: name: Install Dependencies command: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - pip install "pytest-mock==3.12.0" - pip install "pytest==7.3.1" - pip install "pytest-retry==1.6.3" - pip install "pytest-cov==5.0.0" - pip install "pytest-asyncio==0.21.1" - pip install "respx==0.22.0" - pip install "hypercorn==0.17.3" - pip install "pydantic==2.11.0" - pip install "mcp==1.25.0" - pip install "requests-mock>=1.12.1" - pip install "responses==0.25.7" - pip install "pytest-xdist==3.6.1" - pip install "pytest-timeout==2.2.0" - pip install "semantic_router==0.1.10" - pip install "fastapi-offline==1.7.3" - pip install "a2a" + python -m pip install --upgrade pip uv + # Use uv for the heavy requirements.txt (10-100x faster than pip) + uv pip install --system -r requirements.txt + # Use pip for test deps (small set, avoids uv strict-resolution + # conflicts with transitive dep pins like openai<2 and pydantic>=2.11.5) + pip install "pytest-mock==3.12.0" "pytest==7.3.1" "pytest-retry==1.6.3" \ + "pytest-asyncio==0.21.1" "respx==0.22.0" "hypercorn==0.17.3" \ + "pydantic==2.11.0" "mcp==1.26.0" "requests-mock>=1.12.1" \ + "responses==0.25.7" "pytest-xdist==3.6.1" "pytest-timeout==2.2.0" \ + "pytest-cov==5.0.0" "semantic_router==0.1.10" "fastapi-offline==1.7.3" \ + "a2a" "parameterized>=0.9.0" - setup_litellm_enterprise_pip - save_cache: paths: @@ -1367,7 +1360,8 @@ jobs: pip install "pytest-asyncio==0.21.1" pip install "respx==0.22.0" pip install "pydantic==2.11.0" - pip install "mcp==1.25.0" + pip install "mcp==1.26.0" + pip install "pytest-xdist==3.6.1" # Run pytest and generate JUnit XML report - run: name: Run tests @@ -1874,7 +1868,7 @@ jobs: pip install "respx==0.22.0" pip install "hypercorn==0.17.3" pip install "pydantic==2.11.0" - pip install "mcp==1.25.0" + pip install "mcp==1.26.0" pip install "requests-mock>=1.12.1" pip install "responses==0.25.7" pip install "pytest-xdist==3.6.1" @@ -2234,7 +2228,7 @@ jobs: pip install "pytest-asyncio==0.21.1" pip install "pytest-cov==5.0.0" pip install "tomli==2.2.1" - pip install "mcp==1.25.0" + pip install "mcp==1.26.0" - run: name: Run tests command: | diff --git a/.circleci/requirements.txt b/.circleci/requirements.txt index a5ec74424fe..c788dee7410 100644 --- a/.circleci/requirements.txt +++ b/.circleci/requirements.txt @@ -13,7 +13,7 @@ google-cloud-aiplatform==1.43.0 google-cloud-iam==2.19.1 fastapi-sso==0.16.0 uvloop==0.21.0 -mcp==1.25.0 # for MCP server +mcp==1.26.0 # for MCP server semantic_router==0.1.10 # for auto-routing with litellm fastuuid==0.12.0 responses==0.25.7 # for proxy client tests diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 7898f03e015..4c86769c69a 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -169,6 +169,32 @@ if MCP_AVAILABLE: mcp_info: Optional[MCPInfo] = None model_config = ConfigDict(arbitrary_types_allowed=True) + def _normalize_resource_contents(contents: list) -> List[ReadResourceContents]: + """Normalize ResourceContents to ReadResourceContents, preserving meta (MCP 1.26.0+).""" + normalized: List[ReadResourceContents] = [] + for content in contents: + meta = getattr(content, "meta", None) + if meta is None and hasattr(content, "model_dump"): + d = content.model_dump() + meta = d.get("meta") or d.get("_meta") + if isinstance(content, TextResourceContents): + normalized.append( + ReadResourceContents( + content=content.text, + mime_type=content.mimeType, + meta=meta, + ) + ) + elif isinstance(content, BlobResourceContents): + normalized.append( + ReadResourceContents( + content=content.blob, + mime_type=None, + meta=meta, + ) + ) + return normalized + ######################################################## ############ Initialize the MCP Server ################# ######################################################## @@ -623,26 +649,7 @@ if MCP_AVAILABLE: raw_headers=raw_headers, ) - normalized_contents: List[ReadResourceContents] = [] - for content in read_resource_result.contents: - if isinstance(content, TextResourceContents): - text_content: TextResourceContents = content - normalized_contents.append( - ReadResourceContents( - content=text_content.text, - mime_type=text_content.mimeType, - ) - ) - elif isinstance(content, BlobResourceContents): - blob_content: BlobResourceContents = content - normalized_contents.append( - ReadResourceContents( - content=blob_content.blob, - mime_type=None, - ) - ) - - return normalized_contents + return _normalize_resource_contents(read_resource_result.contents) ######################################################## ############ End of MCP Server Routes ################## diff --git a/pyproject.toml b/pyproject.toml index dd8747b6649..86b731c83b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ pynacl = {version = "^1.5.0", optional = true} websockets = {version = "^15.0.1", optional = true} boto3 = { version = "^1.40.76", optional = true } redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} -mcp = {version = ">=1.25.0,<2.0.0", optional = true, python = ">=3.10"} +mcp = {version = ">=1.26.0,<2.0.0", optional = true, python = ">=3.10"} a2a-sdk = {version = "^0.3.22", optional = true, python = ">=3.10"} litellm-proxy-extras = {version = "^0.4.53", optional = true} rich = {version = "^13.7.1", optional = true} diff --git a/requirements.txt b/requirements.txt index ccbfa281d91..bca89f8b387 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ google-cloud-aiplatform==1.133.0 # for vertex ai calls google-cloud-iam==2.19.1 # for GCP IAM Redis authentication google-genai==1.37.0 anthropic[vertex]==0.54.0 -mcp==1.25.0 ; python_version >= "3.10" # for MCP server +mcp==1.26.0 ; python_version >= "3.10" # for MCP server # google-generativeai removed - deprecated, replaced by google-genai (line 21) async_generator==1.10.0 # for async ollama calls langfuse==2.59.7 # for langfuse self-hosted logging diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index a104ac22575..c73b41c7c05 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -1,6 +1,5 @@ import asyncio from datetime import datetime, timedelta -from typing import Optional from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -413,6 +412,57 @@ async def test_mcp_read_resource_success(): assert result is read_result +def test_normalize_resource_contents_passes_metadata(): + """Test that _normalize_resource_contents preserves meta from ResourceContents (MCP 1.26.0+).""" + try: + from litellm.proxy._experimental.mcp_server.server import ( + _normalize_resource_contents, + ) + except ImportError: + pytest.skip("MCP server not available") + + meta = {"version": "1.0", "source": "test"} + contents = [ + TextResourceContents( + uri="https://example.com/resource", + text="hello world", + mimeType="text/plain", + meta=meta, + ) + ] + + result = _normalize_resource_contents(contents) + + assert len(result) == 1 + assert result[0].content == "hello world" + assert result[0].mime_type == "text/plain" + assert result[0].meta == meta + + +def test_normalize_resource_contents_without_metadata(): + """Test that _normalize_resource_contents works when meta is absent (backward compat).""" + try: + from litellm.proxy._experimental.mcp_server.server import ( + _normalize_resource_contents, + ) + except ImportError: + pytest.skip("MCP server not available") + + contents = [ + TextResourceContents( + uri="https://example.com/resource", + text="hello", + mimeType="text/plain", + ) + ] + + result = _normalize_resource_contents(contents) + + assert len(result) == 1 + assert result[0].content == "hello" + assert result[0].meta is None + + @pytest.mark.asyncio async def test_mcp_read_resource_multiple_servers_error(): try: @@ -707,8 +757,6 @@ async def test_concurrent_initialize_session_managers(): """Test that concurrent calls to initialize_session_managers don't cause race conditions.""" try: from litellm.proxy._experimental.mcp_server.server import ( - _INITIALIZATION_LOCK, - _SESSION_MANAGERS_INITIALIZED, initialize_session_managers, ) except ImportError: @@ -1426,7 +1474,6 @@ async def test_list_tools_with_team_tool_permissions_inheritance(): ) from litellm.proxy._types import ( LiteLLM_ObjectPermissionTable, - LiteLLM_TeamTable, UserAPIKeyAuth, ) except ImportError: