mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 05:54:19 +02:00
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:
commit
9b97637e04
5 changed files with 153 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue