From 6d36171f381c1e1a49f3cf98fdbfd145734132af Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 7 Jun 2025 21:39:50 -0400 Subject: [PATCH 1/2] Fix passing token string to client auth --- src/fastmcp/client/transports.py | 17 ++++---- tests/client/test_client.py | 72 ++++++++++++++++++++++++++++++++ tests/server/test_proxy.py | 5 ++- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index a2915fbb4..989d4e296 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -32,6 +32,7 @@ from mcp.shared.memory import create_connected_server_and_client_session 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 @@ class WSTransport(ClientTransport): yield session def __repr__(self) -> str: - return f"" + return f"" class SSETransport(ClientTransport): @@ -183,8 +184,7 @@ class SSETransport(ClientTransport): 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 @@ class SSETransport(ClientTransport): yield session def __repr__(self) -> str: - return f"" + return f"" class StreamableHttpTransport(ClientTransport): @@ -252,8 +252,7 @@ class StreamableHttpTransport(ClientTransport): 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 @@ class StreamableHttpTransport(ClientTransport): yield session def __repr__(self) -> str: - return f"" + return f"" class StdioTransport(ClientTransport): @@ -683,7 +682,7 @@ class FastMCPTransport(ClientTransport): yield session def __repr__(self) -> str: - return f"" + return f"" class MCPConfigTransport(ClientTransport): @@ -769,7 +768,7 @@ class MCPConfigTransport(ClientTransport): yield session def __repr__(self) -> str: - return f"" + return f"" @overload diff --git a/tests/client/test_client.py b/tests/client/test_client.py index e012d8336..6672157c0 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -4,9 +4,11 @@ from typing import cast 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 @@ class TestInferTransport: 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 9ca60f0d8..39e87c8c9 100644 --- a/tests/server/test_proxy.py +++ b/tests/server/test_proxy.py @@ -9,7 +9,7 @@ from pydantic import AnyUrl 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(" Date: Sat, 7 Jun 2025 21:39:57 -0400 Subject: [PATCH 2/2] Support remote auth in mcpconfig --- src/fastmcp/utilities/mcp_config.py | 14 +++++-- tests/utilities/test_mcp_config.py | 59 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py index 5a2990980..6b4b80b4e 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 @@ class RemoteMCPServer(FastMCPBaseModel): 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): diff --git a/tests/utilities/test_mcp_config.py b/tests/utilities/test_mcp_config.py index ec334153d..f5dee613d 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 @@ async def test_multi_client(tmp_path: Path): 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)