From cb7309c89115052cf5d27f08ea979b9b45a7ec26 Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Mon, 22 Jun 2026 16:09:03 +0200 Subject: [PATCH] fix(mcp): handle integer progressToken in host progress capture Per the MCP spec, ProgressToken is `str | int`. The host progress capture path logged the token with `f"...{host_token[:8]}..."`, which raises `TypeError: 'int' object is not subscriptable` when a client sends an integer progress token. The exception was swallowed by the surrounding try/except and surfaced as a misleading "Could not capture host progress context: 'int' object is not subscriptable" warning on every such tool call. The f-string is built eagerly regardless of log level, so the error fired even with debug logging off. The progress callback itself is assigned before the failing log line, so progress forwarding still worked; the only effect was log noise. Wrap the token in str() before slicing so both str and int tokens format safely. --- .../proxy/_experimental/mcp_server/server.py | 2 +- .../mcp_server/test_mcp_server.py | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index e891425274f..e169eff6401 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -714,7 +714,7 @@ if MCP_AVAILABLE: host_progress_callback = forward_progress verbose_logger.debug( - f"Host progressToken captured: {host_token[:8]}..." + f"Host progressToken captured: {str(host_token)[:8]}..." ) except Exception as e: verbose_logger.warning(f"Could not capture host progress context: {e}") 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 f44552c2943..528fa6ff998 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,7 @@ import asyncio import contextvars from datetime import datetime, timedelta +from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -110,6 +111,59 @@ async def test_mcp_server_tool_call_body_contains_request_data(): assert body["arguments"] == tool_arguments +@pytest.mark.asyncio +async def test_mcp_server_tool_call_handles_integer_progress_token(): + """An integer progressToken must not break host progress capture. + + Per the MCP spec ``ProgressToken = str | int``. A client sending an integer + token previously tripped ``f"...{host_token[:8]}..."`` with + ``TypeError: 'int' object is not subscriptable``, which got swallowed into a + misleading "Could not capture host progress context" warning on every such + tool call. Regression test: that warning must not be emitted. + """ + try: + from mcp.server.lowlevel.server import request_ctx + + from litellm.proxy._experimental.mcp_server.server import ( + mcp_server_tool_call, + set_auth_context, + ) + except ImportError: + pytest.skip("MCP server not available") + + set_auth_context(UserAPIKeyAuth(api_key="test_key", user_id="test_user")) + + # Drive the real request context the proxy reads, with an int progressToken + # (e.g. a client SDK that uses an incrementing counter). + host_ctx = SimpleNamespace( + meta=SimpleNamespace(progressToken=12345), + session=MagicMock(send_progress_notification=AsyncMock()), + ) + ctx_token = request_ctx.set(host_ctx) + try: + with patch( + "litellm.proxy._experimental.mcp_server.server.verbose_logger" + ) as mock_logger: + try: + await mcp_server_tool_call("test_tool", {"param": "value"}) + except Exception: + # Downstream tool dispatch is out of scope; we only assert that + # capturing the host progress context did not warn. + pass + finally: + request_ctx.reset(ctx_token) + + progress_warnings = [ + call + for call in mock_logger.warning.call_args_list + if "Could not capture host progress context" in str(call) + ] + assert not progress_warnings, ( + "integer progressToken should be handled gracefully, " + f"got warning(s): {progress_warnings}" + ) + + def test_prepare_mcp_server_headers_case_insensitive_extra_headers(): try: from litellm.proxy._experimental.mcp_server.server import (