Skip to content

Commit 9b97637

Browse files
authored
Merge pull request #754 from jlowin/mcpconfig-auth
Fix passing token string to client auth & add auth to MCPConfig clients
2 parents 43bf19c + 6fbb555 commit 9b97637

File tree

5 files changed

+153
-14
lines changed

5 files changed

+153
-14
lines changed

src/fastmcp/client/transports.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from pydantic import AnyUrl
3333
from typing_extensions import Unpack
3434

35+
from fastmcp.client.auth.bearer import BearerAuth
3536
from fastmcp.client.auth.oauth import OAuth
3637
from fastmcp.server.dependencies import get_http_headers
3738
from fastmcp.server.server import FastMCP
@@ -152,7 +153,7 @@ async def connect_session(
152153
yield session
153154

154155
def __repr__(self) -> str:
155-
return f"<WebSocket(url='{self.url}')>"
156+
return f"<WebSocketTransport(url='{self.url}')>"
156157

157158

158159
class SSETransport(ClientTransport):
@@ -183,8 +184,7 @@ def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
183184
if auth == "oauth":
184185
auth = OAuth(self.url)
185186
elif isinstance(auth, str):
186-
self.headers["Authorization"] = auth
187-
auth = None
187+
auth = BearerAuth(auth)
188188
self.auth = auth
189189

190190
@contextlib.asynccontextmanager
@@ -221,7 +221,7 @@ async def connect_session(
221221
yield session
222222

223223
def __repr__(self) -> str:
224-
return f"<SSE(url='{self.url}')>"
224+
return f"<SSETransport(url='{self.url}')>"
225225

226226

227227
class StreamableHttpTransport(ClientTransport):
@@ -252,8 +252,7 @@ def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
252252
if auth == "oauth":
253253
auth = OAuth(self.url)
254254
elif isinstance(auth, str):
255-
self.headers["Authorization"] = auth
256-
auth = None
255+
auth = BearerAuth(auth)
257256
self.auth = auth
258257

259258
@contextlib.asynccontextmanager
@@ -291,7 +290,7 @@ async def connect_session(
291290
yield session
292291

293292
def __repr__(self) -> str:
294-
return f"<StreamableHttp(url='{self.url}')>"
293+
return f"<StreamableHttpTransport(url='{self.url}')>"
295294

296295

297296
class StdioTransport(ClientTransport):
@@ -683,7 +682,7 @@ async def connect_session(
683682
yield session
684683

685684
def __repr__(self) -> str:
686-
return f"<FastMCP(server='{self.server.name}')>"
685+
return f"<FastMCPTransport(server='{self.server.name}')>"
687686

688687

689688
class MCPConfigTransport(ClientTransport):
@@ -769,7 +768,7 @@ async def connect_session(
769768
yield session
770769

771770
def __repr__(self) -> str:
772-
return f"<MCPConfig(config='{self.config}')>"
771+
return f"<MCPConfigTransport(config='{self.config}')>"
773772

774773

775774
@overload

src/fastmcp/utilities/mcp_config.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any, Literal
3+
from typing import TYPE_CHECKING, Annotated, Any, Literal
44
from urllib.parse import urlparse
55

66
from pydantic import AnyUrl, Field
@@ -56,6 +56,12 @@ class RemoteMCPServer(FastMCPBaseModel):
5656
url: str
5757
headers: dict[str, str] = Field(default_factory=dict)
5858
transport: Literal["streamable-http", "sse", "http"] | None = None
59+
auth: Annotated[
60+
str | Literal["oauth"] | None,
61+
Field(
62+
description='Either a string representing a Bearer token or the literal "oauth" to use OAuth authentication.'
63+
),
64+
] = None
5965

6066
def to_transport(self) -> StreamableHttpTransport | SSETransport:
6167
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
@@ -66,9 +72,11 @@ def to_transport(self) -> StreamableHttpTransport | SSETransport:
6672
transport = self.transport
6773

6874
if transport == "sse":
69-
return SSETransport(self.url, headers=self.headers)
75+
return SSETransport(self.url, headers=self.headers, auth=self.auth)
7076
else:
71-
return StreamableHttpTransport(self.url, headers=self.headers)
77+
return StreamableHttpTransport(
78+
self.url, headers=self.headers, auth=self.auth
79+
)
7280

7381

7482
class MCPConfig(FastMCPBaseModel):

tests/client/test_client.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import pytest
66
from mcp import McpError
7+
from mcp.client.auth import OAuthClientProvider
78
from pydantic import AnyUrl
89

910
from fastmcp.client import Client
11+
from fastmcp.client.auth.bearer import BearerAuth
1012
from fastmcp.client.transports import (
1113
FastMCPTransport,
1214
MCPConfigTransport,
@@ -810,3 +812,73 @@ def test_infer_fastmcp_v1_server(self):
810812
server = FastMCP1()
811813
transport = infer_transport(server)
812814
assert isinstance(transport, FastMCPTransport)
815+
816+
817+
class TestAuth:
818+
def test_default_auth_is_none(self):
819+
client = Client(transport=StreamableHttpTransport("http://localhost:8000"))
820+
assert client.transport.auth is None
821+
822+
def test_stdio_doesnt_support_auth(self):
823+
with pytest.raises(ValueError, match="This transport does not support auth"):
824+
Client(transport=StdioTransport("echo", ["hello"]), auth="oauth")
825+
826+
def test_oauth_literal_sets_up_oauth_shttp(self):
827+
client = Client(
828+
transport=StreamableHttpTransport("http://localhost:8000"), auth="oauth"
829+
)
830+
assert isinstance(client.transport, StreamableHttpTransport)
831+
assert isinstance(client.transport.auth, OAuthClientProvider)
832+
833+
def test_oauth_literal_pass_direct_to_transport(self):
834+
client = Client(
835+
transport=StreamableHttpTransport("http://localhost:8000", auth="oauth"),
836+
)
837+
assert isinstance(client.transport, StreamableHttpTransport)
838+
assert isinstance(client.transport.auth, OAuthClientProvider)
839+
840+
def test_oauth_literal_sets_up_oauth_sse(self):
841+
client = Client(transport=SSETransport("http://localhost:8000"), auth="oauth")
842+
assert isinstance(client.transport, SSETransport)
843+
assert isinstance(client.transport.auth, OAuthClientProvider)
844+
845+
def test_oauth_literal_pass_direct_to_transport_sse(self):
846+
client = Client(transport=SSETransport("http://localhost:8000", auth="oauth"))
847+
assert isinstance(client.transport, SSETransport)
848+
assert isinstance(client.transport.auth, OAuthClientProvider)
849+
850+
def test_auth_string_sets_up_bearer_auth_shttp(self):
851+
client = Client(
852+
transport=StreamableHttpTransport("http://localhost:8000"),
853+
auth="test_token",
854+
)
855+
assert isinstance(client.transport, StreamableHttpTransport)
856+
assert isinstance(client.transport.auth, BearerAuth)
857+
assert client.transport.auth.token.get_secret_value() == "test_token"
858+
859+
def test_auth_string_pass_direct_to_transport_shttp(self):
860+
client = Client(
861+
transport=StreamableHttpTransport(
862+
"http://localhost:8000", auth="test_token"
863+
),
864+
)
865+
assert isinstance(client.transport, StreamableHttpTransport)
866+
assert isinstance(client.transport.auth, BearerAuth)
867+
assert client.transport.auth.token.get_secret_value() == "test_token"
868+
869+
def test_auth_string_sets_up_bearer_auth_sse(self):
870+
client = Client(
871+
transport=SSETransport("http://localhost:8000"),
872+
auth="test_token",
873+
)
874+
assert isinstance(client.transport, SSETransport)
875+
assert isinstance(client.transport.auth, BearerAuth)
876+
assert client.transport.auth.token.get_secret_value() == "test_token"
877+
878+
def test_auth_string_pass_direct_to_transport_sse(self):
879+
client = Client(
880+
transport=SSETransport("http://localhost:8000", auth="test_token"),
881+
)
882+
assert isinstance(client.transport, SSETransport)
883+
assert isinstance(client.transport.auth, BearerAuth)
884+
assert client.transport.auth.token.get_secret_value() == "test_token"

tests/server/test_proxy.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from fastmcp import FastMCP
1111
from fastmcp.client import Client
12-
from fastmcp.client.transports import FastMCPTransport
12+
from fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport
1313
from fastmcp.exceptions import ToolError
1414
from fastmcp.server.proxy import FastMCPProxy
1515

@@ -104,7 +104,8 @@ def test_as_proxy_with_url():
104104
"""FastMCP.as_proxy should accept a URL without connecting."""
105105
proxy = FastMCP.as_proxy("http://example.com/mcp")
106106
assert isinstance(proxy, FastMCPProxy)
107-
assert repr(proxy.client.transport).startswith("<StreamableHttp(")
107+
assert isinstance(proxy.client.transport, StreamableHttpTransport)
108+
assert proxy.client.transport.url == "http://example.com/mcp"
108109

109110

110111
class TestTools:

tests/utilities/test_mcp_config.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import inspect
22
from pathlib import Path
33

4+
from fastmcp.client.auth.bearer import BearerAuth
5+
from fastmcp.client.auth.oauth import OAuthClientProvider
46
from fastmcp.client.client import Client
57
from fastmcp.client.transports import (
68
SSETransport,
@@ -136,3 +138,60 @@ def add(a: int, b: int) -> int:
136138
result_2 = await client.call_tool("test_2_add", {"a": 1, "b": 2})
137139
assert result_1[0].text == "3" # type: ignore[attr-dict]
138140
assert result_2[0].text == "3" # type: ignore[attr-dict]
141+
142+
143+
async def test_remote_config_default_no_auth():
144+
config = {
145+
"mcpServers": {
146+
"test_server": {
147+
"url": "http://localhost:8000",
148+
}
149+
}
150+
}
151+
client = Client(config)
152+
assert isinstance(client.transport.transport, StreamableHttpTransport)
153+
assert client.transport.transport.auth is None
154+
155+
156+
async def test_remote_config_with_auth_token():
157+
config = {
158+
"mcpServers": {
159+
"test_server": {
160+
"url": "http://localhost:8000",
161+
"auth": "test_token",
162+
}
163+
}
164+
}
165+
client = Client(config)
166+
assert isinstance(client.transport.transport, StreamableHttpTransport)
167+
assert isinstance(client.transport.transport.auth, BearerAuth)
168+
assert client.transport.transport.auth.token.get_secret_value() == "test_token"
169+
170+
171+
async def test_remote_config_sse_with_auth_token():
172+
config = {
173+
"mcpServers": {
174+
"test_server": {
175+
"url": "http://localhost:8000/sse",
176+
"auth": "test_token",
177+
}
178+
}
179+
}
180+
client = Client(config)
181+
assert isinstance(client.transport.transport, SSETransport)
182+
assert isinstance(client.transport.transport.auth, BearerAuth)
183+
assert client.transport.transport.auth.token.get_secret_value() == "test_token"
184+
185+
186+
async def test_remote_config_with_oauth_literal():
187+
config = {
188+
"mcpServers": {
189+
"test_server": {
190+
"url": "http://localhost:8000",
191+
"auth": "oauth",
192+
}
193+
}
194+
}
195+
client = Client(config)
196+
assert isinstance(client.transport.transport, StreamableHttpTransport)
197+
assert isinstance(client.transport.transport.auth, OAuthClientProvider)

0 commit comments

Comments
 (0)