Jeremiah Lowin commited on
Commit
bc74b3b
·
unverified ·
2 Parent(s): 1988a4bb86019e

Merge pull request #754 from jlowin/mcpconfig-auth

Browse files

Fix passing token string to client auth & add auth to MCPConfig clients

src/fastmcp/client/transports.py CHANGED
@@ -32,6 +32,7 @@ from mcp.shared.memory import create_connected_server_and_client_session
32
  from pydantic import AnyUrl
33
  from typing_extensions import Unpack
34
 
 
35
  from fastmcp.client.auth.oauth import OAuth
36
  from fastmcp.server.dependencies import get_http_headers
37
  from fastmcp.server.server import FastMCP
@@ -152,7 +153,7 @@ class WSTransport(ClientTransport):
152
  yield session
153
 
154
  def __repr__(self) -> str:
155
- return f"<WebSocket(url='{self.url}')>"
156
 
157
 
158
  class SSETransport(ClientTransport):
@@ -183,8 +184,7 @@ class SSETransport(ClientTransport):
183
  if auth == "oauth":
184
  auth = OAuth(self.url)
185
  elif isinstance(auth, str):
186
- self.headers["Authorization"] = auth
187
- auth = None
188
  self.auth = auth
189
 
190
  @contextlib.asynccontextmanager
@@ -221,7 +221,7 @@ class SSETransport(ClientTransport):
221
  yield session
222
 
223
  def __repr__(self) -> str:
224
- return f"<SSE(url='{self.url}')>"
225
 
226
 
227
  class StreamableHttpTransport(ClientTransport):
@@ -252,8 +252,7 @@ class StreamableHttpTransport(ClientTransport):
252
  if auth == "oauth":
253
  auth = OAuth(self.url)
254
  elif isinstance(auth, str):
255
- self.headers["Authorization"] = auth
256
- auth = None
257
  self.auth = auth
258
 
259
  @contextlib.asynccontextmanager
@@ -291,7 +290,7 @@ class StreamableHttpTransport(ClientTransport):
291
  yield session
292
 
293
  def __repr__(self) -> str:
294
- return f"<StreamableHttp(url='{self.url}')>"
295
 
296
 
297
  class StdioTransport(ClientTransport):
@@ -683,7 +682,7 @@ class FastMCPTransport(ClientTransport):
683
  yield session
684
 
685
  def __repr__(self) -> str:
686
- return f"<FastMCP(server='{self.server.name}')>"
687
 
688
 
689
  class MCPConfigTransport(ClientTransport):
@@ -769,7 +768,7 @@ class MCPConfigTransport(ClientTransport):
769
  yield session
770
 
771
  def __repr__(self) -> str:
772
- return f"<MCPConfig(config='{self.config}')>"
773
 
774
 
775
  @overload
 
32
  from pydantic import AnyUrl
33
  from typing_extensions import Unpack
34
 
35
+ from fastmcp.client.auth.bearer import BearerAuth
36
  from fastmcp.client.auth.oauth import OAuth
37
  from fastmcp.server.dependencies import get_http_headers
38
  from fastmcp.server.server import FastMCP
 
153
  yield session
154
 
155
  def __repr__(self) -> str:
156
+ return f"<WebSocketTransport(url='{self.url}')>"
157
 
158
 
159
  class SSETransport(ClientTransport):
 
184
  if auth == "oauth":
185
  auth = OAuth(self.url)
186
  elif isinstance(auth, str):
187
+ auth = BearerAuth(auth)
 
188
  self.auth = auth
189
 
190
  @contextlib.asynccontextmanager
 
221
  yield session
222
 
223
  def __repr__(self) -> str:
224
+ return f"<SSETransport(url='{self.url}')>"
225
 
226
 
227
  class StreamableHttpTransport(ClientTransport):
 
252
  if auth == "oauth":
253
  auth = OAuth(self.url)
254
  elif isinstance(auth, str):
255
+ auth = BearerAuth(auth)
 
256
  self.auth = auth
257
 
258
  @contextlib.asynccontextmanager
 
290
  yield session
291
 
292
  def __repr__(self) -> str:
293
+ return f"<StreamableHttpTransport(url='{self.url}')>"
294
 
295
 
296
  class StdioTransport(ClientTransport):
 
682
  yield session
683
 
684
  def __repr__(self) -> str:
685
+ return f"<FastMCPTransport(server='{self.server.name}')>"
686
 
687
 
688
  class MCPConfigTransport(ClientTransport):
 
768
  yield session
769
 
770
  def __repr__(self) -> str:
771
+ return f"<MCPConfigTransport(config='{self.config}')>"
772
 
773
 
774
  @overload
src/fastmcp/utilities/mcp_config.py CHANGED
@@ -1,6 +1,6 @@
1
  from __future__ import annotations
2
 
3
- from typing import TYPE_CHECKING, Any, Literal
4
  from urllib.parse import urlparse
5
 
6
  from pydantic import AnyUrl, Field
@@ -56,6 +56,12 @@ class RemoteMCPServer(FastMCPBaseModel):
56
  url: str
57
  headers: dict[str, str] = Field(default_factory=dict)
58
  transport: Literal["streamable-http", "sse", "http"] | None = None
 
 
 
 
 
 
59
 
60
  def to_transport(self) -> StreamableHttpTransport | SSETransport:
61
  from fastmcp.client.transports import SSETransport, StreamableHttpTransport
@@ -66,9 +72,11 @@ class RemoteMCPServer(FastMCPBaseModel):
66
  transport = self.transport
67
 
68
  if transport == "sse":
69
- return SSETransport(self.url, headers=self.headers)
70
  else:
71
- return StreamableHttpTransport(self.url, headers=self.headers)
 
 
72
 
73
 
74
  class MCPConfig(FastMCPBaseModel):
 
1
  from __future__ import annotations
2
 
3
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
4
  from urllib.parse import urlparse
5
 
6
  from pydantic import AnyUrl, Field
 
56
  url: str
57
  headers: dict[str, str] = Field(default_factory=dict)
58
  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
65
 
66
  def to_transport(self) -> StreamableHttpTransport | SSETransport:
67
  from fastmcp.client.transports import SSETransport, StreamableHttpTransport
 
72
  transport = self.transport
73
 
74
  if transport == "sse":
75
+ return SSETransport(self.url, headers=self.headers, auth=self.auth)
76
  else:
77
+ return StreamableHttpTransport(
78
+ self.url, headers=self.headers, auth=self.auth
79
+ )
80
 
81
 
82
  class MCPConfig(FastMCPBaseModel):
tests/client/test_client.py CHANGED
@@ -4,9 +4,11 @@ from typing import cast
4
 
5
  import pytest
6
  from mcp import McpError
 
7
  from pydantic import AnyUrl
8
 
9
  from fastmcp.client import Client
 
10
  from fastmcp.client.transports import (
11
  FastMCPTransport,
12
  MCPConfigTransport,
@@ -810,3 +812,73 @@ class TestInferTransport:
810
  server = FastMCP1()
811
  transport = infer_transport(server)
812
  assert isinstance(transport, FastMCPTransport)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  import pytest
6
  from mcp import McpError
7
+ from mcp.client.auth import OAuthClientProvider
8
  from pydantic import AnyUrl
9
 
10
  from fastmcp.client import Client
11
+ from fastmcp.client.auth.bearer import BearerAuth
12
  from fastmcp.client.transports import (
13
  FastMCPTransport,
14
  MCPConfigTransport,
 
812
  server = FastMCP1()
813
  transport = infer_transport(server)
814
  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 CHANGED
@@ -9,7 +9,7 @@ from pydantic import AnyUrl
9
 
10
  from fastmcp import FastMCP
11
  from fastmcp.client import Client
12
- from fastmcp.client.transports import FastMCPTransport
13
  from fastmcp.exceptions import ToolError
14
  from fastmcp.server.proxy import FastMCPProxy
15
 
@@ -104,7 +104,8 @@ def test_as_proxy_with_url():
104
  """FastMCP.as_proxy should accept a URL without connecting."""
105
  proxy = FastMCP.as_proxy("http://example.com/mcp")
106
  assert isinstance(proxy, FastMCPProxy)
107
- assert repr(proxy.client.transport).startswith("<StreamableHttp(")
 
108
 
109
 
110
  class TestTools:
 
9
 
10
  from fastmcp import FastMCP
11
  from fastmcp.client import Client
12
+ from fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport
13
  from fastmcp.exceptions import ToolError
14
  from fastmcp.server.proxy import FastMCPProxy
15
 
 
104
  """FastMCP.as_proxy should accept a URL without connecting."""
105
  proxy = FastMCP.as_proxy("http://example.com/mcp")
106
  assert isinstance(proxy, FastMCPProxy)
107
+ assert isinstance(proxy.client.transport, StreamableHttpTransport)
108
+ assert proxy.client.transport.url == "http://example.com/mcp"
109
 
110
 
111
  class TestTools:
tests/utilities/test_mcp_config.py CHANGED
@@ -1,6 +1,8 @@
1
  import inspect
2
  from pathlib import Path
3
 
 
 
4
  from fastmcp.client.client import Client
5
  from fastmcp.client.transports import (
6
  SSETransport,
@@ -136,3 +138,60 @@ async def test_multi_client(tmp_path: Path):
136
  result_2 = await client.call_tool("test_2_add", {"a": 1, "b": 2})
137
  assert result_1[0].text == "3" # type: ignore[attr-dict]
138
  assert result_2[0].text == "3" # type: ignore[attr-dict]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import inspect
2
  from pathlib import Path
3
 
4
+ from fastmcp.client.auth.bearer import BearerAuth
5
+ from fastmcp.client.auth.oauth import OAuthClientProvider
6
  from fastmcp.client.client import Client
7
  from fastmcp.client.transports import (
8
  SSETransport,
 
138
  result_2 = await client.call_tool("test_2_add", {"a": 1, "b": 2})
139
  assert result_1[0].text == "3" # type: ignore[attr-dict]
140
  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)