Merge pull request #754 from jlowin/mcpconfig-auth

Fix passing token string to client auth & add auth to MCPConfig clients
This commit is contained in:
Jeremiah Lowin 2025-06-07 21:44:06 -04:00 committed by GitHub
commit 9b97637e04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 153 additions and 14 deletions

View file

@ -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"<WebSocket(url='{self.url}')>"
return f"<WebSocketTransport(url='{self.url}')>"
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"<SSE(url='{self.url}')>"
return f"<SSETransport(url='{self.url}')>"
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"<StreamableHttp(url='{self.url}')>"
return f"<StreamableHttpTransport(url='{self.url}')>"
class StdioTransport(ClientTransport):
@ -683,7 +682,7 @@ class FastMCPTransport(ClientTransport):
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 @@ class MCPConfigTransport(ClientTransport):
yield session
def __repr__(self) -> str:
return f"<MCPConfig(config='{self.config}')>"
return f"<MCPConfigTransport(config='{self.config}')>"
@overload

View file

@ -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):

View file

@ -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"

View file

@ -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("<StreamableHttp(")
assert isinstance(proxy.client.transport, StreamableHttpTransport)
assert proxy.client.transport.url == "http://example.com/mcp"
class TestTools:

View file

@ -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)