Skip to content

Conversation

@guschnwg
Copy link

@guschnwg guschnwg commented Oct 31, 2025

When trying to connect to a MCP server that is in a domain without a valid certificate, the first POST to /mcp worked (with a correct 401 unauthorized) and we are moved to the authentication flow.

OAuth fails when doing the Pre-flight check before opening the browser because it was using an httpx.AsyncClient with the default verify=True even tho we setup the Transport to use a client with verify=False.

from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport
import httpx
import asyncio

client = Client(
    StreamableHttpTransport(
        url="https://url-for-server-with-invalid-certificate.com/mcp",
        httpx_client_factory=lambda *args, **kwargs: httpx.AsyncClient(verify=False, *args, **kwargs),
        auth="oauth",
    ),
)

async def call_tool():
    async with client:
        result = await client.call_tool("who_am_i")
        print(result)

asyncio.run(call_tool())

Expected behavior:

[10/31/25 16:56:43] INFO     OAuth authorization URL:                                                                                                                                                       oauth.py:247
                             https://url-for-server-with-invalid-certificate.com/authorize?response_type=code&client_id=CLIENT-ID&redirect_uri=http%3A%2F%2Flo
                             calhost%3A56751%2Fcallback&state=STATE&code_challenge=CODE_CHALLENGE&code_challenge_method=S256&resource=ht
                             tps%3A%2F%2Furl-for-server-with-invalid-certificate.com
                    INFO     🎧 OAuth callback server started on http://localhost:56751

Current behavior:

ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'mcp-adroll-com-staging-1485208707.us-west-2.elb.amazonaws.com'. (_ssl.c:1010)

Server logs (same for both approaches, but with the fix it finished the oauth):

INFO:     172.18.162.145:36194 - "GET /ping HTTP/1.1" 200 OK
INFO:     172.18.224.224:49784 - "POST /mcp HTTP/1.1" 401 Unauthorized
[10/31/25 19:56:11] INFO     Auth error returned: invalid_token middleware.py:92
                             (status=401)
INFO:     172.18.224.224:49786 - "GET /ping HTTP/1.1" 200 OK
INFO:     172.18.224.224:49794 - "GET /.well-known/oauth-protected-resource/mcp HTTP/1.1" 200 OK
INFO:     172.18.224.224:49794 - "GET /.well-known/oauth-authorization-server HTTP/1.1" 200 OK

Summary by CodeRabbit

  • Refactor

    • OAuth authentication now supports custom HTTP client configuration through injectable client factories, enabling consistent client behavior across authentication flows.
  • Tests

    • Added validation test to verify OAuth uses matching HTTP client configuration with transport settings.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 31, 2025

Warning

Rate limit exceeded

@guschnwg has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 4 minutes and 5 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d9325f8 and e4a2fff.

📒 Files selected for processing (1)
  • tests/client/transports/test_transports.py (1 hunks)

Walkthrough

OAuth now accepts an optional httpx_client_factory; transports pass their client factory into OAuth during initialization. Transport initialization assigns the factory before setting auth. A test verifies OAuth uses the transport-provided HTTPX client (e.g., with custom SSL settings).

Changes

Cohort / File(s) Summary
OAuth constructor & client usage
src/fastmcp/client/auth/oauth.py
Added httpx_client_factory parameter to OAuth.__init__ and stored it as self.httpx_client_factory; defaulted internal client creation to httpx.AsyncClient when no factory is provided; redirect_handler uses the factory to create HTTP clients.
Transports — pass factory to OAuth
src/fastmcp/client/transports.py
Assigned httpx_client_factory before calling _set_auth; when auth == "oauth", instantiate OAuth(self.url, httpx_client_factory=self.httpx_client_factory) so OAuth receives the transport's factory.
Tests — verify factory propagation
tests/client/transports/test_transports.py
Added async test that constructs a StreamableHttpTransport with a custom httpx_client_factory (returns httpx.AsyncClient(verify=False)), sets auth="oauth", and asserts the OAuth-used client reflects the transport's SSL verification setting.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Transport as StreamableHttpTransport
    participant OAuth as OAuth
    participant Factory as httpx_client_factory

    App->>Transport: new StreamableHttpTransport(..., httpx_client_factory=factory)
    Transport->>Transport: self.httpx_client_factory = factory
    Transport->>Transport: _set_auth("oauth")
    Transport->>OAuth: new OAuth(url, httpx_client_factory=factory)
    OAuth->>OAuth: store self.httpx_client_factory

    rect rgba(200,220,240,0.3)
      Note over OAuth,Factory: when performing redirect/requests
      OAuth->>Factory: call httpx_client_factory()
      Factory-->>OAuth: AsyncClient(verify=False)
    end
Loading

Poem

🐰 A rabbit hops into the code,
Bringing a factory on the road.
Transport and OAuth link paw to paw,
Custom clients now steward the law.
TLS whispers: "Trust—or not!" 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "Allow OAuth instance to use the same httpx factory as the Transport" is clear, concise, and directly describes the main change in the changeset. The title accurately reflects that OAuth has been modified to accept and use an httpx client factory parameter passed from the Transport, which is the core objective of this PR. The title is specific enough to convey the primary change without being overly verbose.
Description Check ✅ Passed The pull request description provides an excellent and comprehensive explanation of the problem, including reproduction code, expected versus actual behavior, and server logs demonstrating the fix. The description clearly articulates why the change is needed (OAuth was not respecting the transport's httpx client factory configuration, causing certificate verification failures). However, the description does not include the procedural checklist items from the template (Contributors Checklist and Review Checklist are not completed), and no issue number is referenced. Despite these missing procedural sections, the substantive description content is detailed and complete, and the raw summary confirms that tests were added to validate the changes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@marvin-context-protocol marvin-context-protocol bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. client Related to the FastMCP client SDK or client-side functionality. labels Oct 31, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/fastmcp/client/auth/oauth.py (1)

41-69: Critical: NameError in standalone function.

Line 50 references self.httpx_client_factory, but check_if_auth_required is a module-level function, not a method. The function has no self parameter, so this will raise a NameError at runtime.

If this function is meant to be used internally by OAuth, convert it to an instance method:

-async def check_if_auth_required(
-    mcp_url: str, httpx_kwargs: dict[str, Any] | None = None
-) -> bool:
+    async def check_if_auth_required(
+        self, httpx_kwargs: dict[str, Any] | None = None
+    ) -> bool:
-    """
-    Check if the MCP endpoint requires authentication by making a test request.
+        """
+        Check if the MCP endpoint requires authentication by making a test request.
 
-    Returns:
-        True if auth appears to be required, False otherwise
-    """
-    async with self.httpx_client_factory(**(httpx_kwargs or {})) as client:
-        try:
-            # Try a simple request to the endpoint
-            response = await client.get(mcp_url, timeout=5.0)
+        Returns:
+            True if auth appears to be required, False otherwise
+        """
+        async with self.httpx_client_factory(**(httpx_kwargs or {})) as client:
+            try:
+                # Try a simple request to the endpoint
+                response = await client.get(self.url, timeout=5.0)

If it's meant to be a standalone utility function, pass httpx_client_factory as a parameter instead of referencing self.

src/fastmcp/client/transports.py (1)

178-192: Critical: Initialization order bug and missing OAuth parameter in SSETransport.

Two related issues prevent SSETransport from correctly passing httpx_client_factory to OAuth:

  1. Line 181 assigns self.httpx_client_factory AFTER line 180 calls self._set_auth(auth), so the factory is unavailable when OAuth is created.
  2. Line 189 creates OAuth without passing the httpx_client_factory parameter.

This means SSETransport with auth="oauth" will always use the default httpx.AsyncClient instead of the custom factory, breaking certificate verification customization described in the PR objectives.

Apply this diff to fix both issues:

         self.url = url
         self.headers = headers or {}
+        self.httpx_client_factory = httpx_client_factory
         self._set_auth(auth)
-        self.httpx_client_factory = httpx_client_factory

         if isinstance(sse_read_timeout, int | float):
             sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
         self.sse_read_timeout = sse_read_timeout

     def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
         if auth == "oauth":
-            auth = OAuth(self.url)
+            auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
         elif isinstance(auth, str):
             auth = BearerAuth(auth)
         self.auth = auth
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 443c44c and d37c97a.

📒 Files selected for processing (3)
  • src/fastmcp/client/auth/oauth.py (5 hunks)
  • src/fastmcp/client/transports.py (1 hunks)
  • tests/client/transports/test_transports.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use Python ≥ 3.10 and provide full type annotations for library code

Files:

  • src/fastmcp/client/auth/oauth.py
  • src/fastmcp/client/transports.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Never use bare except; always catch specific exception types

Files:

  • src/fastmcp/client/auth/oauth.py
  • src/fastmcp/client/transports.py
  • tests/client/transports/test_transports.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.py: Tests must be atomic, self-contained, and cover a single functionality
Use pytest parameterization for multiple examples of the same functionality
Use separate tests for distinct pieces of functionality
Always put imports at the top of test files; do not import inside test bodies
Do not add @pytest.mark.asyncio; asyncio_mode = "auto" is set globally
Prefer in-memory transport for tests; use HTTP transport only when explicitly testing networking
For slow/long-running tests, mark them as integration or optimize (default timeout is 5s)
In tests, use # type: ignore[attr-defined] for MCP results instead of type assertions

Files:

  • tests/client/transports/test_transports.py
🧠 Learnings (1)
📚 Learning: 2025-10-27T14:40:00.422Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-27T14:40:00.422Z
Learning: Applies to tests/**/*.py : Prefer in-memory transport for tests; use HTTP transport only when explicitly testing networking

Applied to files:

  • tests/client/transports/test_transports.py
🧬 Code graph analysis (2)
src/fastmcp/client/transports.py (1)
src/fastmcp/client/auth/oauth.py (1)
  • OAuth (135-324)
tests/client/transports/test_transports.py (1)
src/fastmcp/client/transports.py (1)
  • StreamableHttpTransport (229-297)
🔇 Additional comments (3)
src/fastmcp/client/auth/oauth.py (2)

143-169: LGTM!

The OAuth constructor correctly accepts and stores the httpx_client_factory parameter with a sensible default fallback to httpx.AsyncClient.


229-248: LGTM!

The redirect_handler correctly uses the configured httpx_client_factory for the pre-flight authorization URL check.

src/fastmcp/client/transports.py (1)

248-262: LGTM!

StreamableHttpTransport correctly assigns self.httpx_client_factory before calling self._set_auth(auth) and passes it to the OAuth constructor. This ensures OAuth uses the same HTTP client configuration as the transport.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d37c97a and d9325f8.

📒 Files selected for processing (2)
  • src/fastmcp/client/auth/oauth.py (4 hunks)
  • tests/client/transports/test_transports.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Never use bare except; always catch specific exception types

Files:

  • tests/client/transports/test_transports.py
  • src/fastmcp/client/auth/oauth.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.py: Tests must be atomic, self-contained, and cover a single functionality
Use pytest parameterization for multiple examples of the same functionality
Use separate tests for distinct pieces of functionality
Always put imports at the top of test files; do not import inside test bodies
Do not add @pytest.mark.asyncio; asyncio_mode = "auto" is set globally
Prefer in-memory transport for tests; use HTTP transport only when explicitly testing networking
For slow/long-running tests, mark them as integration or optimize (default timeout is 5s)
In tests, use # type: ignore[attr-defined] for MCP results instead of type assertions

Files:

  • tests/client/transports/test_transports.py
src/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use Python ≥ 3.10 and provide full type annotations for library code

Files:

  • src/fastmcp/client/auth/oauth.py
🧠 Learnings (1)
📚 Learning: 2025-10-27T14:40:00.422Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-27T14:40:00.422Z
Learning: Applies to tests/**/*.py : Prefer in-memory transport for tests; use HTTP transport only when explicitly testing networking

Applied to files:

  • tests/client/transports/test_transports.py
🧬 Code graph analysis (1)
tests/client/transports/test_transports.py (2)
src/fastmcp/client/client.py (1)
  • Client (97-941)
src/fastmcp/client/transports.py (1)
  • StreamableHttpTransport (229-297)
🪛 GitHub Actions: Run static analysis
tests/client/transports/test_transports.py

[error] 15-15: ruff-check: Local variable 'client' is assigned to but never used (F841).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
Copy link
Owner

@jlowin jlowin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @guschnwg - in the future, please follow the repo requirements and open an issue first. We will generally close PRs without issues even if they are valid.


def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
if auth == "oauth":
auth = OAuth(self.url)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be added to the SSE transport as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. client Related to the FastMCP client SDK or client-side functionality.

2 participants