From f672481277eebc7e4d6409f5557c81f415dabc65 Mon Sep 17 00:00:00 2001 From: Javier Martinez Date: Mon, 3 Aug 2026 10:31:41 +0200 Subject: [PATCH] fix: mcp oauth error (#2325) * fix: rebuild httpx2 exceptions to an controllable exception * fix: avoid to create loop before to control the env * chore: enable verbose * chore: run all tests --- .github/workflows/tests.yml | 2 +- Makefile | 2 +- private_gpt/celery/base.py | 3 ++- .../server/chat/interceptors/mcp_interceptor.py | 5 +++-- private_gpt/server/mcp/_runtime.py | 14 ++++++++++++++ 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c87a69b3..a5428a2d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,7 @@ jobs: if: ${{ !startsWith(github.head_ref, 'release-please--branches--') }} env: GIT_TARGET: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || 'HEAD^' }} - run: make test-changed PYTEST_ARGS="--git-target $GIT_TARGET --junit-xml=tests-results.xml" + run: make test PYTEST_ARGS="--git-target $GIT_TARGET --junit-xml=tests-results.xml" - name: Run full test suite with coverage if: ${{ startsWith(github.head_ref, 'release-please--branches--') }} run: make test-coverage diff --git a/Makefile b/Makefile index f9111c85..68aedf91 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ WIPE_LOCAL_DATA_DIR := $(WIPE_PGPT_HOME)/local_data test: rm -rf "$(TEST_LOCAL_DATA_DIR)"/* - PGPT_HOME=$(TEST_PGPT_HOME) PYTHONPATH=. uv run pytest tests $(PYTEST_ARGS) + PGPT_HOME=$(TEST_PGPT_HOME) PYTHONPATH=. uv run pytest tests $(PYTEST_ARGS) -v test-changed: rm -rf "$(TEST_LOCAL_DATA_DIR)"/* diff --git a/private_gpt/celery/base.py b/private_gpt/celery/base.py index 4374a536..7671417f 100644 --- a/private_gpt/celery/base.py +++ b/private_gpt/celery/base.py @@ -369,12 +369,13 @@ class StatefulBackgroundTask(_BackgroundTask): async def _warm_async(cls) -> None: from private_gpt.eager_loading import warm - injector = create_loop_injector() profile = os.environ.get("PGPT_WORKER_WARM_PROFILE", "").strip() if not profile: raise ValueError( "PGPT_WORKER_WARM_PROFILE is required for stateful workers" ) + + injector = create_loop_injector() warm(injector, profile=profile) @classmethod diff --git a/private_gpt/server/chat/interceptors/mcp_interceptor.py b/private_gpt/server/chat/interceptors/mcp_interceptor.py index b53baf5e..cc49fd56 100644 --- a/private_gpt/server/chat/interceptors/mcp_interceptor.py +++ b/private_gpt/server/chat/interceptors/mcp_interceptor.py @@ -1,6 +1,7 @@ import asyncio -from httpx import HTTPStatusError +import httpx +import httpx2 from injector import inject, singleton from private_gpt.components.chat.models.chat_config_models import ( @@ -23,7 +24,7 @@ from private_gpt.server.mcp.mcp_service import McpService, mcp_tool_to_spec def _extract_original_exception(exc: BaseException) -> BaseException: - if isinstance(exc, HTTPStatusError): + if isinstance(exc, (httpx.HTTPStatusError, httpx2.HTTPStatusError)): if exc.response.status_code in (401, 403): return PermissionError( f"MCP server rejected the request with HTTP {exc.response.status_code}. " diff --git a/private_gpt/server/mcp/_runtime.py b/private_gpt/server/mcp/_runtime.py index 5a51ba0c..42743778 100644 --- a/private_gpt/server/mcp/_runtime.py +++ b/private_gpt/server/mcp/_runtime.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any from urllib.parse import urlparse if TYPE_CHECKING: + import httpx2 from mcp import ClientSession, MCPError from mcp.types import ( AudioContent, @@ -15,6 +16,7 @@ if TYPE_CHECKING: TextContent, ) else: + import httpx2 from mcp import ClientSession, MCPError from mcp.client.sse import sse_client from mcp.client.stdio import StdioServerParameters, stdio_client @@ -54,6 +56,14 @@ def _prefer_sse(url: str) -> bool: return path.endswith("/sse") or "/sse/" in path +async def _check_auth(url: str, headers: dict[str, Any]) -> None: + """Do a pre-flight POST to detect 401/403 before entering the MCP transport.""" + async with httpx2.AsyncClient(follow_redirects=True, timeout=10.0) as client: + response = await client.post(url, headers=headers, content=b"{}") + if response.status_code in (401, 403): + response.raise_for_status() + + class PersistentMCPClient: """Native MCP 2.x client with persistent session recovery. @@ -107,6 +117,7 @@ class PersistentMCPClient: ) ) else: + await _check_auth(self.command_or_url, self.headers) http_client = create_mcp_http_client() if self.headers: http_client.headers.update(self.headers) @@ -169,6 +180,9 @@ class PersistentMCPClient: ) yield session return + except httpx2.HTTPStatusError: + await self._reset_session() + raise except (MCPError, ConnectionError, TimeoutError, OSError) as e: last_exception = e logger.warning(