Repository: jlowin/fastmcp

Issue:
No Auth Support for MCPConfigTransport
### Description

Hi I found on current 2.7.0, if I am init a Client through MCPConfigTransport, it does not have auth support. Is this something we are going to maybe in a way to be included through config so later we can use .to_transport() to infer? 

### Example Code

```Python

```

### Version Information

```Text
2.7.0
```

### Additional Context

_No response_


Discussion:
Thank you, this is a good call. While working on it I found a related bug!



Patch:
diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py
index a2915fbb..989d4e29 100644
--- a/src/fastmcp/client/transports.py
+++ b/src/fastmcp/client/transports.py
@@ -32,6 +32,7 @@
 from pydantic import AnyUrl
 from typing_extensions import Unpack
 
+from fastmcp.client.auth.bearer import BearerAuth
 from fastmcp.client.auth.oauth import OAuth
 from fastmcp.server.dependencies import get_http_headers
 from fastmcp.server.server import FastMCP
@@ -152,7 +153,7 @@ async def connect_session(
                 yield session
 
     def __repr__(self) -> str:
-        return f"<WebSocket(url='{self.url}')>"
+        return f"<WebSocketTransport(url='{self.url}')>"
 
 
 class SSETransport(ClientTransport):
@@ -183,8 +184,7 @@ def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
         if auth == "oauth":
             auth = OAuth(self.url)
         elif isinstance(auth, str):
-            self.headers["Authorization"] = auth
-            auth = None
+            auth = BearerAuth(auth)
         self.auth = auth
 
     @contextlib.asynccontextmanager
@@ -221,7 +221,7 @@ async def connect_session(
                 yield session
 
     def __repr__(self) -> str:
-        return f"<SSE(url='{self.url}')>"
+        return f"<SSETransport(url='{self.url}')>"
 
 
 class StreamableHttpTransport(ClientTransport):
@@ -252,8 +252,7 @@ def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
         if auth == "oauth":
             auth = OAuth(self.url)
         elif isinstance(auth, str):
-            self.headers["Authorization"] = auth
-            auth = None
+            auth = BearerAuth(auth)
         self.auth = auth
 
     @contextlib.asynccontextmanager
@@ -291,7 +290,7 @@ async def connect_session(
                 yield session
 
     def __repr__(self) -> str:
-        return f"<StreamableHttp(url='{self.url}')>"
+        return f"<StreamableHttpTransport(url='{self.url}')>"
 
 
 class StdioTransport(ClientTransport):
@@ -683,7 +682,7 @@ async def connect_session(
             yield session
 
     def __repr__(self) -> str:
-        return f"<FastMCP(server='{self.server.name}')>"
+        return f"<FastMCPTransport(server='{self.server.name}')>"
 
 
 class MCPConfigTransport(ClientTransport):
@@ -769,7 +768,7 @@ async def connect_session(
             yield session
 
     def __repr__(self) -> str:
-        return f"<MCPConfig(config='{self.config}')>"
+        return f"<MCPConfigTransport(config='{self.config}')>"
 
 
 @overload
diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py
index 5a299098..6b4b80b4 100644
--- a/src/fastmcp/utilities/mcp_config.py
+++ b/src/fastmcp/utilities/mcp_config.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Literal
+from typing import TYPE_CHECKING, Annotated, Any, Literal
 from urllib.parse import urlparse
 
 from pydantic import AnyUrl, Field
@@ -56,6 +56,12 @@ class RemoteMCPServer(FastMCPBaseModel):
     url: str
     headers: dict[str, str] = Field(default_factory=dict)
     transport: Literal["streamable-http", "sse", "http"] | None = None
+    auth: Annotated[
+        str | Literal["oauth"] | None,
+        Field(
+            description='Either a string representing a Bearer token or the literal "oauth" to use OAuth authentication.'
+        ),
+    ] = None
 
     def to_transport(self) -> StreamableHttpTransport | SSETransport:
         from fastmcp.client.transports import SSETransport, StreamableHttpTransport
@@ -66,9 +72,11 @@ def to_transport(self) -> StreamableHttpTransport | SSETransport:
             transport = self.transport
 
         if transport == "sse":
-            return SSETransport(self.url, headers=self.headers)
+            return SSETransport(self.url, headers=self.headers, auth=self.auth)
         else:
-            return StreamableHttpTransport(self.url, headers=self.headers)
+            return StreamableHttpTransport(
+                self.url, headers=self.headers, auth=self.auth
+            )
 
 
 class MCPConfig(FastMCPBaseModel):


Test patch:
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index e012d833..6672157c 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -4,9 +4,11 @@
 
 import pytest
 from mcp import McpError
+from mcp.client.auth import OAuthClientProvider
 from pydantic import AnyUrl
 
 from fastmcp.client import Client
+from fastmcp.client.auth.bearer import BearerAuth
 from fastmcp.client.transports import (
     FastMCPTransport,
     MCPConfigTransport,
@@ -810,3 +812,73 @@ def test_infer_fastmcp_v1_server(self):
         server = FastMCP1()
         transport = infer_transport(server)
         assert isinstance(transport, FastMCPTransport)
+
+
+class TestAuth:
+    def test_default_auth_is_none(self):
+        client = Client(transport=StreamableHttpTransport("http://localhost:8000"))
+        assert client.transport.auth is None
+
+    def test_stdio_doesnt_support_auth(self):
+        with pytest.raises(ValueError, match="This transport does not support auth"):
+            Client(transport=StdioTransport("echo", ["hello"]), auth="oauth")
+
+    def test_oauth_literal_sets_up_oauth_shttp(self):
+        client = Client(
+            transport=StreamableHttpTransport("http://localhost:8000"), auth="oauth"
+        )
+        assert isinstance(client.transport, StreamableHttpTransport)
+        assert isinstance(client.transport.auth, OAuthClientProvider)
+
+    def test_oauth_literal_pass_direct_to_transport(self):
+        client = Client(
+            transport=StreamableHttpTransport("http://localhost:8000", auth="oauth"),
+        )
+        assert isinstance(client.transport, StreamableHttpTransport)
+        assert isinstance(client.transport.auth, OAuthClientProvider)
+
+    def test_oauth_literal_sets_up_oauth_sse(self):
+        client = Client(transport=SSETransport("http://localhost:8000"), auth="oauth")
+        assert isinstance(client.transport, SSETransport)
+        assert isinstance(client.transport.auth, OAuthClientProvider)
+
+    def test_oauth_literal_pass_direct_to_transport_sse(self):
+        client = Client(transport=SSETransport("http://localhost:8000", auth="oauth"))
+        assert isinstance(client.transport, SSETransport)
+        assert isinstance(client.transport.auth, OAuthClientProvider)
+
+    def test_auth_string_sets_up_bearer_auth_shttp(self):
+        client = Client(
+            transport=StreamableHttpTransport("http://localhost:8000"),
+            auth="test_token",
+        )
+        assert isinstance(client.transport, StreamableHttpTransport)
+        assert isinstance(client.transport.auth, BearerAuth)
+        assert client.transport.auth.token.get_secret_value() == "test_token"
+
+    def test_auth_string_pass_direct_to_transport_shttp(self):
+        client = Client(
+            transport=StreamableHttpTransport(
+                "http://localhost:8000", auth="test_token"
+            ),
+        )
+        assert isinstance(client.transport, StreamableHttpTransport)
+        assert isinstance(client.transport.auth, BearerAuth)
+        assert client.transport.auth.token.get_secret_value() == "test_token"
+
+    def test_auth_string_sets_up_bearer_auth_sse(self):
+        client = Client(
+            transport=SSETransport("http://localhost:8000"),
+            auth="test_token",
+        )
+        assert isinstance(client.transport, SSETransport)
+        assert isinstance(client.transport.auth, BearerAuth)
+        assert client.transport.auth.token.get_secret_value() == "test_token"
+
+    def test_auth_string_pass_direct_to_transport_sse(self):
+        client = Client(
+            transport=SSETransport("http://localhost:8000", auth="test_token"),
+        )
+        assert isinstance(client.transport, SSETransport)
+        assert isinstance(client.transport.auth, BearerAuth)
+        assert client.transport.auth.token.get_secret_value() == "test_token"
diff --git a/tests/server/test_proxy.py b/tests/server/test_proxy.py
index 9ca60f0d..39e87c8c 100644
--- a/tests/server/test_proxy.py
+++ b/tests/server/test_proxy.py
@@ -9,7 +9,7 @@
 
 from fastmcp import FastMCP
 from fastmcp.client import Client
-from fastmcp.client.transports import FastMCPTransport
+from fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport
 from fastmcp.exceptions import ToolError
 from fastmcp.server.proxy import FastMCPProxy
 
@@ -104,7 +104,8 @@ def test_as_proxy_with_url():
     """FastMCP.as_proxy should accept a URL without connecting."""
     proxy = FastMCP.as_proxy("http://example.com/mcp")
     assert isinstance(proxy, FastMCPProxy)
-    assert repr(proxy.client.transport).startswith("<StreamableHttp(")
+    assert isinstance(proxy.client.transport, StreamableHttpTransport)
+    assert proxy.client.transport.url == "http://example.com/mcp"
 
 
 class TestTools:
diff --git a/tests/utilities/test_mcp_config.py b/tests/utilities/test_mcp_config.py
index ec334153..f5dee613 100644
--- a/tests/utilities/test_mcp_config.py
+++ b/tests/utilities/test_mcp_config.py
@@ -1,6 +1,8 @@
 import inspect
 from pathlib import Path
 
+from fastmcp.client.auth.bearer import BearerAuth
+from fastmcp.client.auth.oauth import OAuthClientProvider
 from fastmcp.client.client import Client
 from fastmcp.client.transports import (
     SSETransport,
@@ -136,3 +138,60 @@ def add(a: int, b: int) -> int:
         result_2 = await client.call_tool("test_2_add", {"a": 1, "b": 2})
         assert result_1[0].text == "3"  # type: ignore[attr-dict]
         assert result_2[0].text == "3"  # type: ignore[attr-dict]
+
+
+async def test_remote_config_default_no_auth():
+    config = {
+        "mcpServers": {
+            "test_server": {
+                "url": "http://localhost:8000",
+            }
+        }
+    }
+    client = Client(config)
+    assert isinstance(client.transport.transport, StreamableHttpTransport)
+    assert client.transport.transport.auth is None
+
+
+async def test_remote_config_with_auth_token():
+    config = {
+        "mcpServers": {
+            "test_server": {
+                "url": "http://localhost:8000",
+                "auth": "test_token",
+            }
+        }
+    }
+    client = Client(config)
+    assert isinstance(client.transport.transport, StreamableHttpTransport)
+    assert isinstance(client.transport.transport.auth, BearerAuth)
+    assert client.transport.transport.auth.token.get_secret_value() == "test_token"
+
+
+async def test_remote_config_sse_with_auth_token():
+    config = {
+        "mcpServers": {
+            "test_server": {
+                "url": "http://localhost:8000/sse",
+                "auth": "test_token",
+            }
+        }
+    }
+    client = Client(config)
+    assert isinstance(client.transport.transport, SSETransport)
+    assert isinstance(client.transport.transport.auth, BearerAuth)
+    assert client.transport.transport.auth.token.get_secret_value() == "test_token"
+
+
+async def test_remote_config_with_oauth_literal():
+    config = {
+        "mcpServers": {
+            "test_server": {
+                "url": "http://localhost:8000",
+                "auth": "oauth",
+            }
+        }
+    }
+    client = Client(config)
+    assert isinstance(client.transport.transport, StreamableHttpTransport)
+    assert isinstance(client.transport.transport.auth, OAuthClientProvider)