diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..d7be3b7fc
--- /dev/null
+++ b/.github/dependabot.yml
@@ -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"
diff --git a/.github/release.yml b/.github/release.yml
index 32eb2588e..e637ece59 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -27,6 +27,10 @@ changelog:
labels:
- documentation
+ - title: Dependencies 📦
+ labels:
+ - dependencies
+
- title: Other Changes 🦾
labels:
- "*"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 66ab30968..49a55b17d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -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
diff --git a/.github/workflows/run-static.yml b/.github/workflows/run-static.yml
index 526844f6c..bb4ade8f4 100644
--- a/.github/workflows/run-static.yml
+++ b/.github/workflows/run-static.yml
@@ -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"
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 04f35da3e..cf4805e87 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -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"
diff --git a/docs/assets/updates/release-2-7.png b/docs/assets/updates/release-2-7.png
new file mode 100644
index 000000000..5716e1f73
Binary files /dev/null and b/docs/assets/updates/release-2-7.png differ
diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx
index 041040b00..c5cdaea69 100644
--- a/docs/integrations/anthropic.mdx
+++ b/docs/integrations/anthropic.mdx
@@ -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).
-### 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.
-### 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
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.
diff --git a/docs/integrations/claude-desktop.mdx b/docs/integrations/claude-desktop.mdx
index 9b8dc5404..9c4729dbe 100644
--- a/docs/integrations/claude-desktop.mdx
+++ b/docs/integrations/claude-desktop.mdx
@@ -1,5 +1,5 @@
---
-title: Claude Desktop
+title: Claude Desktop + FastMCP
sidebarTitle: Claude Desktop
description: Call FastMCP servers from Claude Desktop
icon: desktop
diff --git a/docs/integrations/contrib.mdx b/docs/integrations/contrib.mdx
index aead6affd..2f71c0b61 100644
--- a/docs/integrations/contrib.mdx
+++ b/docs/integrations/contrib.mdx
@@ -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
diff --git a/docs/integrations/gemini.mdx b/docs/integrations/gemini.mdx
index 2f5cc5e3d..fee4a9d8e 100644
--- a/docs/integrations/gemini.mdx
+++ b/docs/integrations/gemini.mdx
@@ -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
diff --git a/docs/integrations/openai.mdx b/docs/integrations/openai.mdx
index a5b1e933f..b68ebcc18 100644
--- a/docs/integrations/openai.mdx
+++ b/docs/integrations/openai.mdx
@@ -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.
-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.
diff --git a/docs/updates.mdx b/docs/updates.mdx
index 1faf9dc91..afaac40e7 100644
--- a/docs/updates.mdx
+++ b/docs/updates.mdx
@@ -3,11 +3,36 @@ title: "FastMCP Updates"
sidebarTitle: "Updates"
icon: "sparkles"
tag: "New!"
+mode: "wide"
---
+
+
+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.
+
+
+
+
+
-
-
-
str:
- return f""
+ return f""
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""
+ return f""
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""
+ return f""
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""
+ return f""
class MCPConfigTransport(ClientTransport):
@@ -769,7 +776,7 @@ class MCPConfigTransport(ClientTransport):
yield session
def __repr__(self) -> str:
- return f""
+ return f""
@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):
diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py
index 572af5282..08775225e 100644
--- a/src/fastmcp/server/dependencies.py
+++ b/src/fastmcp/server/dependencies.py
@@ -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",
diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py
index 2b5381437..b9121dfe8 100644
--- a/src/fastmcp/server/http.py
+++ b/src/fastmcp/server/http.py
@@ -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,
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 12fbf976d..5cda3cbb4 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -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)
diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py
index 96939d11c..f39c82ff1 100644
--- a/src/fastmcp/settings.py
+++ b/src/fastmcp/settings.py
@@ -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
diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py
index 5a2990980..176cb097d 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
@@ -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):
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index e012d8336..1c246337d 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -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"
diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py
index 090666ccc..97c2efd3e 100644
--- a/tests/server/test_mount.py
+++ b/tests/server/test_mount.py
@@ -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."""
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("