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
This commit is contained in:
Javier Martinez
2026-08-03 10:31:41 +02:00
committed by GitHub
parent 0216db9f4a
commit f672481277
5 changed files with 21 additions and 5 deletions

View File

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

View File

@@ -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)"/*

View File

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

View File

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

View File

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