Merge branch 'main' into transform-tools-2

This commit is contained in:
Jeremiah Lowin 2025-06-09 17:24:23 -04:00 committed by GitHub
commit 47b2c58134
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 302 additions and 83 deletions

20
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"

4
.github/release.yml vendored
View file

@ -27,6 +27,10 @@ changelog:
labels:
- documentation
- title: Dependencies 📦
labels:
- dependencies
- title: Other Changes 🦾
labels:
- "*"

View file

@ -17,7 +17,7 @@ jobs:
fetch-depth: 0
- name: "Install uv"
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v6
- name: Build
run: uv build

View file

@ -32,7 +32,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

View file

@ -37,7 +37,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

View file

@ -1,6 +1,6 @@
---
title: Anthropic
sidebarTitle: Anthropic
title: Anthropic API + FastMCP
sidebarTitle: Anthropic API
description: Call FastMCP servers from the Anthropic API
icon: message-smile
tag: "New!"
@ -8,9 +8,6 @@ tag: "New!"
import { VersionBadge } from "/snippets/version-badge.mdx"
Anthropic supports MCP servers through the [MCP connector](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector) feature in the Messages API, allowing you to extend AI capabilities with custom tools from remote MCP servers.
## Messages API
Anthropic's [Messages API](https://docs.anthropic.com/en/api/messages) supports MCP servers as remote tool sources. This tutorial will show you how to create a FastMCP server and deploy it to a public URL, then how to call it from the Messages API.
@ -18,7 +15,7 @@ Anthropic's [Messages API](https://docs.anthropic.com/en/api/messages) supports
Currently, the MCP connector only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to Claude. Other MCP features like resources and prompts are not currently supported. You can read more about the MCP connector in the [Anthropic documentation](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector).
</Tip>
### Create a Server
## Create a Server
First, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.
@ -37,7 +34,7 @@ if __name__ == "__main__":
mcp.run(transport="sse", port=8000)
```
### Deploy the Server
## Deploy the Server
Your server must be deployed to a public URL in order for Anthropic to access it. The MCP connector supports both SSE and Streamable HTTP transports.
@ -59,7 +56,7 @@ ngrok http 8000
This exposes your unauthenticated server to the internet. Only run this command in a safe environment if you understand the risks.
</Warning>
### Call the Server
## Call the Server
To use the Messages API with MCP servers, you'll need to install the Anthropic Python SDK (not included with FastMCP):
@ -114,13 +111,13 @@ The results were 4, 2, and 6. Would you like me to roll again or roll a differen
```
### Authentication
## Authentication
<VersionBadge version="2.6.0" />
The MCP connector supports OAuth authentication through authorization tokens, which means you can secure your server while still allowing Anthropic to access it.
#### Server Authentication
### Server Authentication
The simplest way to add authentication to the server is to use a bearer token scheme.
@ -181,7 +178,7 @@ if __name__ == "__main__":
mcp.run(transport="sse", port=8000)
```
#### Client Authentication
### Client Authentication
If you try to call the authenticated server with the same Anthropic code we wrote earlier, you'll get an error indicating that the server rejected the request because it's not authenticated.

View file

@ -1,5 +1,5 @@
---
title: Claude Desktop
title: Claude Desktop + FastMCP
sidebarTitle: Claude Desktop
description: Call FastMCP servers from Claude Desktop
icon: desktop

View file

@ -12,7 +12,7 @@ FastMCP includes a `contrib` package that holds community-contributed modules. T
Contrib modules provide additional features, integrations, or patterns that complement the core FastMCP library. They offer a way for the community to share useful extensions while keeping the core library focused and maintainable.
The available modules can be viewed in the [contrib directory](https://github.com/jlowin/fastmcp/tree/main/src/contrib).
The available modules can be viewed in the [contrib directory](https://github.com/jlowin/fastmcp/tree/main/src/fastmcp/contrib).
## Usage

View file

@ -1,5 +1,5 @@
---
title: Gemini SDK
title: Gemini SDK + FastMCP
sidebarTitle: Gemini SDK
description: Call FastMCP servers from the Google Gemini SDK
icon: message-smile

View file

@ -1,6 +1,6 @@
---
title: OpenAI
sidebarTitle: OpenAI
title: OpenAI API + FastMCP
sidebarTitle: OpenAI API
description: Call FastMCP servers from the OpenAI API
icon: message-smile
tag: "New!"
@ -8,14 +8,13 @@ tag: "New!"
import { VersionBadge } from "/snippets/version-badge.mdx"
OpenAI recently announced support for MCP servers in the Responses API. Note that at this time, MCP is not supported in ChatGPT.
## Responses API
OpenAI's [Responses API](https://platform.openai.com/docs/api-reference/responses) supports [MCP servers](https://platform.openai.com/docs/guides/tools-remote-mcp) as remote tool sources, allowing you to extend AI capabilities with custom functions.
<Note>
The Responses API is a distinct API from OpenAI's Completions API, Assistants API, or ChatGPT. At this time, only the Responses API supports MCP.
The Responses API is a distinct API from OpenAI's Completions API or Assistants API. At this time, only the Responses API supports MCP.
</Note>
<Tip>

View file

@ -3,11 +3,36 @@ title: "FastMCP Updates"
sidebarTitle: "Updates"
icon: "sparkles"
tag: "New!"
mode: "wide"
---
<Update label="FastMCP 2.7" description="June 6, 2025">
<Card
title="FastMCP 2.7: Pare Programming" href="https://github.com/jlowin/fastmcp/releases/tag/v2.7.0"
img="assets/updates/release-2-7.png"
cta="Read the release notes"
arrow="false"
>
FastMCP 2.7 has been released!
Most notably, it introduces the highly requested (and Pythonic) "naked" decorator usage:
```python {3}
mcp = FastMCP()
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
```
In addition, decorators now return the objects they create, instead of the decorated function. This is an important usability enhancement.
The bulk of the update is focused on improving the FastMCP internals, including a few breaking internal changes to private APIs. A number of functions that have clung on since 1.0 are now deprecated.
</Card>
</Update>
<Update label="FastMCP 2.6" description="June 6, 2025">
<Card
title="Blast Auth with FastMCP 2.6" href="https://www.jlowin.dev/blog/fastmcp-2-6"
img="https://www.jlowin.dev/_image?href=%2F_astro%2Fhero.Bsu8afiw.png&w=1000&h=500&f=webp"

View file

@ -1,4 +1,4 @@
"""FastmMCP CLI tools."""
"""FastMCP CLI tools."""
import importlib.metadata
import importlib.util

View file

@ -145,6 +145,7 @@ class Client(Generic[ClientTransportT]):
progress_handler: ProgressHandler | None = None,
timeout: datetime.timedelta | float | int | None = None,
init_timeout: datetime.timedelta | float | int | None = None,
client_info: mcp.types.Implementation | None = None,
auth: httpx.Auth | Literal["oauth"] | str | None = None,
):
self.transport = cast(ClientTransportT, infer_transport(transport))
@ -180,6 +181,7 @@ class Client(Generic[ClientTransportT]):
"logging_callback": create_log_callback(log_handler),
"message_handler": message_handler,
"read_timeout_seconds": timeout,
"client_info": client_info,
}
if roots is not None:

View file

@ -8,39 +8,25 @@ import sys
import warnings
from collections.abc import AsyncIterator, Callable
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Literal,
TypedDict,
TypeVar,
cast,
overload,
)
from typing import Any, Literal, TypedDict, TypeVar, cast, overload
import anyio
import httpx
import mcp.types
from mcp import ClientSession, StdioServerParameters
from mcp.client.session import (
ListRootsFnT,
LoggingFnT,
MessageHandlerFnT,
SamplingFnT,
)
from mcp.client.session import ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
from mcp.server.fastmcp import FastMCP as FastMCP1Server
from mcp.shared.memory import create_connected_server_and_client_session
from mcp.shared.memory import create_client_server_memory_streams
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
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
if TYPE_CHECKING:
from fastmcp.utilities.mcp_config import MCPConfig
logger = get_logger(__name__)
# TypeVar for preserving specific ClientTransport subclass types
@ -64,11 +50,12 @@ __all__ = [
class SessionKwargs(TypedDict, total=False):
"""Keyword arguments for the MCP ClientSession constructor."""
read_timeout_seconds: datetime.timedelta | None
sampling_callback: SamplingFnT | None
list_roots_callback: ListRootsFnT | None
logging_callback: LoggingFnT | None
message_handler: MessageHandlerFnT | None
read_timeout_seconds: datetime.timedelta | None
client_info: mcp.types.Implementation | None
class ClientTransport(abc.ABC):
@ -152,7 +139,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 +170,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 +207,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 +238,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 +276,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):
@ -663,27 +648,49 @@ class FastMCPTransport(ClientTransport):
tests or scenarios where client and server run in the same runtime.
"""
def __init__(self, mcp: FastMCP | FastMCP1Server):
def __init__(self, mcp: FastMCP | FastMCP1Server, raise_exceptions: bool = False):
"""Initialize a FastMCPTransport from a FastMCP server instance."""
# Accept both FastMCP 2.x and FastMCP 1.0 servers. Both expose a
# ``_mcp_server`` attribute pointing to the underlying MCP server
# implementation, so we can treat them identically.
self.server = mcp
self.raise_exceptions = raise_exceptions
@contextlib.asynccontextmanager
async def connect_session(
self, **session_kwargs: Unpack[SessionKwargs]
) -> AsyncIterator[ClientSession]:
# create_connected_server_and_client_session manages the session lifecycle itself
async with create_connected_server_and_client_session(
server=self.server._mcp_server,
**session_kwargs,
) as session:
yield session
async with create_client_server_memory_streams() as (
client_streams,
server_streams,
):
client_read, client_write = client_streams
server_read, server_write = server_streams
# Create a cancel scope for the server task
async with anyio.create_task_group() as tg:
tg.start_soon(
lambda: self.server._mcp_server.run(
server_read,
server_write,
self.server._mcp_server.create_initialization_options(),
raise_exceptions=self.raise_exceptions,
)
)
try:
async with ClientSession(
read_stream=client_read,
write_stream=client_write,
**session_kwargs,
) as client_session:
yield client_session
finally:
tg.cancel_scope.cancel()
def __repr__(self) -> str:
return f"<FastMCP(server='{self.server.name}')>"
return f"<FastMCPTransport(server='{self.server.name}')>"
class MCPConfigTransport(ClientTransport):
@ -769,7 +776,7 @@ class MCPConfigTransport(ClientTransport):
yield session
def __repr__(self) -> str:
return f"<MCPConfig(config='{self.config}')>"
return f"<MCPConfigTransport(config='{self.config}')>"
@overload
@ -860,7 +867,6 @@ def infer_transport(
transport = infer_transport(config)
```
"""
from fastmcp.utilities.mcp_config import MCPConfig
# the transport is already a ClientTransport
if isinstance(transport, ClientTransport):

View file

@ -67,6 +67,7 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
"te",
"keep-alive",
"expect",
"accept",
# Proxy-related headers
"proxy-authenticate",
"proxy-authorization",

View file

@ -13,6 +13,7 @@ from mcp.server.auth.middleware.bearer_auth import (
from mcp.server.auth.routes import create_auth_routes
from mcp.server.lowlevel.server import LifespanResultT
from mcp.server.sse import SseServerTransport
from mcp.server.streamable_http import EventStore
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.middleware import Middleware
@ -241,7 +242,7 @@ def create_sse_app(
def create_streamable_http_app(
server: FastMCP[LifespanResultT],
streamable_http_path: str,
event_store: None = None,
event_store: EventStore | None = None,
auth: OAuthProvider | None = None,
json_response: bool = False,
stateless_http: bool = False,

View file

@ -41,6 +41,7 @@ from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import BaseRoute, Route
import fastmcp
import fastmcp.server
import fastmcp.settings
from fastmcp.exceptions import NotFoundError
@ -131,6 +132,8 @@ class FastMCP(Generic[LifespanResultT]):
tools: list[Tool | Callable[..., Any]] | None = None,
**settings: Any,
):
if cache_expiration_seconds is not None:
settings["cache_expiration_seconds"] = cache_expiration_seconds
self.settings = fastmcp.settings.ServerSettings(**settings)
# If mask_error_details is provided, override the settings value
@ -148,7 +151,9 @@ class FastMCP(Generic[LifespanResultT]):
self.tags: set[str] = tags or set()
self.dependencies = dependencies
self._cache = TimedCache(
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
expiration=datetime.timedelta(
seconds=self.settings.cache_expiration_seconds
)
)
self._mounted_servers: dict[str, MountedServer] = {}
self._additional_http_routes: list[BaseRoute] = []
@ -496,11 +501,7 @@ class FastMCP(Generic[LifespanResultT]):
with the Context type annotation. See the @tool decorator for examples.
Args:
fn: The function to register as a tool
name: Optional name for the tool (defaults to function name)
description: Optional description of what the tool does
tags: Optional set of tags for categorizing the tool
annotations: Optional annotations about the tool's behavior
tool: The Tool instance to register
"""
self._tool_manager.add_tool(tool)
self._cache.clear()
@ -870,7 +871,7 @@ class FastMCP(Generic[LifespanResultT]):
This decorator supports multiple calling patterns:
- @server.prompt (without parentheses)
- @server.prompt (with empty parentheses)
- @server.prompt() (with empty parentheses)
- @server.prompt("custom_name") (with name as first argument)
- @server.prompt(name="custom_name") (with name as keyword argument)
- server.prompt(function, name="custom_name") (direct function call)
@ -892,7 +893,7 @@ class FastMCP(Generic[LifespanResultT]):
}
]
@server.prompt
@server.prompt()
def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
ctx.info(f"Analyzing table {table_name}")
schema = read_table_schema(table_name)

View file

@ -170,7 +170,7 @@ class ServerSettings(BaseSettings):
),
] = []
# cache settings (for checking mounted servers)
# cache settings (for getting attributes from servers, used to avoid repeated calls)
cache_expiration_seconds: float = 0
# StreamableHTTP settings

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
@ -55,7 +55,13 @@ class StdioMCPServer(FastMCPBaseModel):
class RemoteMCPServer(FastMCPBaseModel):
url: str
headers: dict[str, str] = Field(default_factory=dict)
transport: Literal["streamable-http", "sse", "http"] | None = None
transport: Literal["streamable-http", "sse"] | 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

@ -1,12 +1,16 @@
import asyncio
import sys
from typing import cast
from unittest.mock import AsyncMock
import mcp
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,
@ -273,6 +277,14 @@ async def test_client_connection(fastmcp_server):
assert not client.is_connected()
async def test_initialize_called_once(fastmcp_server, monkeypatch):
mock_initialize = AsyncMock()
monkeypatch.setattr(mcp.ClientSession, "initialize", mock_initialize)
client = Client(transport=FastMCPTransport(fastmcp_server))
async with client:
assert mock_initialize.call_count == 1
async def test_initialize_result_connected(fastmcp_server):
"""Test that initialize_result returns the correct result when connected."""
client = Client(transport=FastMCPTransport(fastmcp_server))
@ -810,3 +822,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

@ -304,6 +304,19 @@ class TestDynamicChanges:
tools = await main_app.get_tools()
assert "sub_temp_tool" not in tools
async def test_cache_expiration(self):
main_app = FastMCP("MainApp", cache_expiration_seconds=2)
sub_app = FastMCP("SubApp")
tools = await main_app.get_tools()
assert len(tools) == 0
@sub_app.tool
def sub_tool():
return "sub_tool"
tools = await main_app.get_tools()
assert len(tools) == 0
class TestResourcesAndTemplates:
"""Test mounting with resources and resource templates."""

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

@ -617,7 +617,7 @@ class TestToolContextInjection:
result = await client.call_tool("tool_with_context", {"x": 42})
assert len(result) == 1
content = result[0]
assert content.text == "2" # type: ignore[attr-defined]
assert content.text == "1" # type: ignore[attr-defined]
async def test_async_context(self):
"""Test that context works in async functions."""
@ -632,7 +632,7 @@ class TestToolContextInjection:
result = await client.call_tool("async_tool", {"x": 42})
assert len(result) == 1
content = result[0]
assert content.text == "Async request 2: 42" # type: ignore[attr-defined]
assert content.text == "Async request 1: 42" # type: ignore[attr-defined]
async def test_optional_context(self):
"""Test that context is optional."""
@ -696,7 +696,7 @@ class TestToolContextInjection:
async with Client(mcp) as client:
result = await client.call_tool("MyTool", {"x": 2})
assert result[0].text == "4" # type: ignore[attr-defined]
assert result[0].text == "3" # type: ignore[attr-defined]
class TestResource:
@ -780,7 +780,7 @@ class TestResourceContext:
async with Client(mcp) as client:
result = await client.read_resource(AnyUrl("resource://test"))
assert result[0].text == "2" # type: ignore[attr-defined]
assert result[0].text == "1" # type: ignore[attr-defined]
class TestResourceTemplates:
@ -1015,7 +1015,7 @@ class TestResourceTemplateContext:
async with Client(mcp) as client:
result = await client.read_resource(AnyUrl("resource://test"))
assert result[0].text.startswith("Resource template: test 2") # type: ignore[attr-defined]
assert result[0].text.startswith("Resource template: test 1") # type: ignore[attr-defined]
async def test_resource_template_context_with_callable_object(self):
mcp = FastMCP()
@ -1031,7 +1031,7 @@ class TestResourceTemplateContext:
async with Client(mcp) as client:
result = await client.read_resource(AnyUrl("resource://test"))
assert result[0].text.startswith("Resource template: test 2") # type: ignore[attr-defined]
assert result[0].text.startswith("Resource template: test 1") # type: ignore[attr-defined]
class TestPrompts:
@ -1249,4 +1249,4 @@ class TestPromptContext:
assert len(result.messages) == 1
message = result.messages[0]
assert message.role == "user"
assert message.content.text == "Hello, World! 2" # type: ignore[attr-defined]
assert message.content.text == "Hello, World! 1" # type: ignore[attr-defined]

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)