diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx
index 70c47bfa9..c53ecee03 100644
--- a/docs/clients/client.mdx
+++ b/docs/clients/client.mdx
@@ -91,7 +91,7 @@ For more control over connection details (like headers for SSE, environment vari
### Multi-Server Clients
-
+
FastMCP supports creating clients that connect to multiple MCP servers through a single client interface using a standard MCP configuration format (`MCPConfig`). This configuration approach makes it easy to connect to multiple specialized servers or create composable systems with a simple, declarative syntax.
diff --git a/docs/clients/transports.mdx b/docs/clients/transports.mdx
index b89f46884..3669c8b25 100644
--- a/docs/clients/transports.mdx
+++ b/docs/clients/transports.mdx
@@ -323,7 +323,7 @@ Communication happens through efficient in-memory queues, making it very fast an
### MCPConfig Transport
-
+
- **Class:** `fastmcp.client.transports.MCPConfigTransport`
- **Inferred From:** An instance of `MCPConfig` or a dictionary matching the MCPConfig schema
diff --git a/docs/servers/composition.mdx b/docs/servers/composition.mdx
index dc584bced..73b40affc 100644
--- a/docs/servers/composition.mdx
+++ b/docs/servers/composition.mdx
@@ -35,7 +35,7 @@ The choice of importing or mounting depends on your use case and requirements.
FastMCP supports [MCP proxying](/patterns/proxy), which allows you to mirror a local or remote server in a local FastMCP instance. Proxies are fully compatible with both importing and mounting.
-
+
You can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time.
diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx
index 3a84286e8..555a0c105 100644
--- a/docs/servers/context.mdx
+++ b/docs/servers/context.mdx
@@ -228,8 +228,8 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
# Create a sampling prompt asking for sentiment analysis
prompt = f"Analyze the sentiment of the following text as positive, negative, or neutral. Just output a single word - 'positive', 'negative', or 'neutral'. Text to analyze: {text}"
- # Send the sampling request to the client's LLM
- response = await ctx.sample(prompt)
+ # Send the sampling request to the client's LLM (provide a hint for the model you want to use)
+ response = await ctx.sample(prompt, model_preferences="claude-3-sonnet")
# Process the LLM's response
sentiment = response.text.strip().lower()
@@ -247,11 +247,12 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
**Method signature:**
-- **`ctx.sample(messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None) -> TextContent | ImageContent`**
+- **`ctx.sample(messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None) -> TextContent | ImageContent`**
- `messages`: A string or list of strings/message objects to send to the LLM
- `system_prompt`: Optional system prompt to guide the LLM's behavior
- `temperature`: Optional sampling temperature (controls randomness)
- `max_tokens`: Optional maximum number of tokens to generate (defaults to 512)
+ - `model_preferences`: Optional model selection preferences (e.g., a model hint string, list of hints, or a ModelPreferences object)
- Returns the LLM's response as TextContent or ImageContent
When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles.
diff --git a/docs/servers/proxy.mdx b/docs/servers/proxy.mdx
index ef2899cd2..d78d6d694 100644
--- a/docs/servers/proxy.mdx
+++ b/docs/servers/proxy.mdx
@@ -106,7 +106,7 @@ proxy = FastMCP.as_proxy(
### Configuration-Based Proxies
-
+
You can create a proxy directly from a configuration dictionary that follows the MCPConfig schema. This is useful for quickly setting up proxies to remote servers without manually configuring each connection detail.
diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py
index 19552ea35..f6333fb08 100644
--- a/src/fastmcp/client/client.py
+++ b/src/fastmcp/client/client.py
@@ -210,6 +210,23 @@ class Client:
result = await self.session.send_ping()
return isinstance(result, mcp.types.EmptyResult)
+ async def cancel(
+ self,
+ request_id: str | int,
+ reason: str | None = None,
+ ) -> None:
+ """Send a cancellation notification for an in-progress request."""
+ notification = mcp.types.ClientNotification(
+ mcp.types.CancelledNotification(
+ method="notifications/cancelled",
+ params=mcp.types.CancelledNotificationParams(
+ requestId=request_id,
+ reason=reason,
+ ),
+ )
+ )
+ await self.session.send_notification(notification)
+
async def progress(
self,
progress_token: str | int,
diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py
index 7aacd5ad6..f82b238d0 100644
--- a/src/fastmcp/client/transports.py
+++ b/src/fastmcp/client/transports.py
@@ -535,8 +535,12 @@ class MCPConfigTransport(ClientTransport):
config = MCPConfig.from_dict(config)
self.config = config
+ # if there are no servers, raise an error
+ if len(self.config.mcpServers) == 0:
+ raise ValueError("No MCP servers defined in the config")
+
# if there's exactly one server, create a client for that server
- if len(self.config.mcpServers) == 1:
+ elif len(self.config.mcpServers) == 1:
self.transport = list(self.config.mcpServers.values())[0].to_transport()
# otherwise create a composite client
diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py
index 4ecd992a7..7dc77f4d1 100644
--- a/src/fastmcp/server/context.py
+++ b/src/fastmcp/server/context.py
@@ -12,6 +12,8 @@ from mcp.shared.context import RequestContext
from mcp.types import (
CreateMessageResult,
ImageContent,
+ ModelHint,
+ ModelPreferences,
Root,
SamplingMessage,
TextContent,
@@ -200,6 +202,7 @@ class Context:
system_prompt: str | None = None,
temperature: float | None = None,
max_tokens: int | None = None,
+ model_preferences: ModelPreferences | str | list[str] | None = None,
) -> TextContent | ImageContent:
"""
Send a sampling request to the client and await the response.
@@ -231,6 +234,7 @@ class Context:
system_prompt=system_prompt,
temperature=temperature,
max_tokens=max_tokens,
+ model_preferences=self._parse_model_preferences(model_preferences),
)
return result.content
@@ -248,3 +252,45 @@ class Context:
)
return fastmcp.server.dependencies.get_http_request()
+
+ def _parse_model_preferences(
+ self, model_preferences: ModelPreferences | str | list[str] | None
+ ) -> ModelPreferences | None:
+ """
+ Validates and converts user input for model_preferences into a ModelPreferences object.
+
+ Args:
+ model_preferences (ModelPreferences | str | list[str] | None):
+ The model preferences to use. Accepts:
+ - ModelPreferences (returns as-is)
+ - str (single model hint)
+ - list[str] (multiple model hints)
+ - None (no preferences)
+
+ Returns:
+ ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
+
+ Raises:
+ ValueError: If the input is not a supported type or contains invalid values.
+ """
+ if model_preferences is None:
+ return None
+ elif isinstance(model_preferences, ModelPreferences):
+ return model_preferences
+ elif isinstance(model_preferences, str):
+ # Single model hint
+ return ModelPreferences(hints=[ModelHint(name=model_preferences)])
+ elif isinstance(model_preferences, list):
+ # List of model hints (strings)
+ if not all(isinstance(h, str) for h in model_preferences):
+ raise ValueError(
+ "All elements of model_preferences list must be"
+ " strings (model name hints)."
+ )
+ return ModelPreferences(
+ hints=[ModelHint(name=h) for h in model_preferences]
+ )
+ else:
+ raise ValueError(
+ "model_preferences must be one of: ModelPreferences, str, list[str], or None."
+ )
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index e9ab55b5a..111d3eaa3 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -998,7 +998,7 @@ class FastMCP(Generic[LifespanResultT]):
from fastmcp.server.proxy import FastMCPProxy
if tool_separator is not None:
- # Deprecated since 2.3.6
+ # Deprecated since 2.4.0
warnings.warn(
"The tool_separator parameter is deprecated and will be removed in a future version. "
"Tools are now prefixed using 'prefix_toolname' format.",
@@ -1007,7 +1007,7 @@ class FastMCP(Generic[LifespanResultT]):
)
if resource_separator is not None:
- # Deprecated since 2.3.6
+ # Deprecated since 2.4.0
warnings.warn(
"The resource_separator parameter is deprecated and ignored. "
"Resource prefixes are now added using the protocol://prefix/path format.",
@@ -1016,7 +1016,7 @@ class FastMCP(Generic[LifespanResultT]):
)
if prompt_separator is not None:
- # Deprecated since 2.3.6
+ # Deprecated since 2.4.0
warnings.warn(
"The prompt_separator parameter is deprecated and will be removed in a future version. "
"Prompts are now prefixed using 'prefix_promptname' format.",
@@ -1083,7 +1083,7 @@ class FastMCP(Generic[LifespanResultT]):
prompt_separator: Deprecated. Separator for prompt names.
"""
if tool_separator is not None:
- # Deprecated since 2.3.6
+ # Deprecated since 2.4.0
warnings.warn(
"The tool_separator parameter is deprecated and will be removed in a future version. "
"Tools are now prefixed using 'prefix_toolname' format.",
@@ -1092,7 +1092,7 @@ class FastMCP(Generic[LifespanResultT]):
)
if resource_separator is not None:
- # Deprecated since 2.3.6
+ # Deprecated since 2.4.0
warnings.warn(
"The resource_separator parameter is deprecated and ignored. "
"Resource prefixes are now added using the protocol://prefix/path format.",
@@ -1101,7 +1101,7 @@ class FastMCP(Generic[LifespanResultT]):
)
if prompt_separator is not None:
- # Deprecated since 2.3.6
+ # Deprecated since 2.4.0
warnings.warn(
"The prompt_separator parameter is deprecated and will be removed in a future version. "
"Prompts are now prefixed using 'prefix_promptname' format.",
diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py
index 19905f701..a9f5abee6 100644
--- a/src/fastmcp/utilities/mcp_config.py
+++ b/src/fastmcp/utilities/mcp_config.py
@@ -32,11 +32,12 @@ def infer_transport_type_from_url(
return "streamable-http"
-class LocalMCPServer(BaseModel):
+class StdioMCPServer(BaseModel):
command: str
args: list[str] = Field(default_factory=list)
env: dict[str, Any] = Field(default_factory=dict)
cwd: str | None = None
+ transport: Literal["stdio"] = "stdio"
def to_transport(self) -> StdioTransport:
from fastmcp.client.transports import StdioTransport
@@ -51,8 +52,8 @@ class LocalMCPServer(BaseModel):
class RemoteMCPServer(BaseModel):
url: str
- transport: Literal["streamable-http", "sse", "http"] | None = None
headers: dict[str, str] = Field(default_factory=dict)
+ transport: Literal["streamable-http", "sse", "http"] | None = None
def to_transport(self) -> StreamableHttpTransport | SSETransport:
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
@@ -69,7 +70,7 @@ class RemoteMCPServer(BaseModel):
class MCPConfig(BaseModel):
- mcpServers: dict[str, LocalMCPServer | RemoteMCPServer]
+ mcpServers: dict[str, StdioMCPServer | RemoteMCPServer]
@classmethod
def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index 0e97cb8d6..71625d45e 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -673,6 +673,18 @@ class TestInferTransport:
assert transport.transport.command == "echo"
assert transport.transport.args == ["hello"]
+ def test_config_with_no_servers(self):
+ """Test that an empty MCPConfig raises a ValueError."""
+ config = {"mcpServers": {}}
+ with pytest.raises(ValueError, match="No MCP servers defined in the config"):
+ infer_transport(config)
+
+ def test_mcpconfigtransport_with_no_servers(self):
+ """Test that MCPConfigTransport raises a ValueError when initialized with an empty config."""
+ config = {"mcpServers": {}}
+ with pytest.raises(ValueError, match="No MCP servers defined in the config"):
+ MCPConfigTransport(config=config)
+
def test_infer_composite_client(self):
config = {
"mcpServers": {
diff --git a/tests/server/test_context.py b/tests/server/test_context.py
index 4243ab4f9..a41b8b6e3 100644
--- a/tests/server/test_context.py
+++ b/tests/server/test_context.py
@@ -2,9 +2,11 @@ import warnings
from unittest.mock import MagicMock, patch
import pytest
+from mcp.types import ModelPreferences
from starlette.requests import Request
from fastmcp.server.context import Context
+from fastmcp.server.server import FastMCP
class TestContextDeprecations:
@@ -57,3 +59,30 @@ class TestContextDeprecations:
assert "https://gofastmcp.com/patterns/http-requests" in str(
warning.message
)
+
+
+@pytest.fixture
+def context():
+ return Context(fastmcp=FastMCP())
+
+
+class TestParseModelPreferences:
+ def test_parse_model_preferences_string(self, context):
+ mp = context._parse_model_preferences("claude-3-sonnet")
+ assert isinstance(mp, ModelPreferences)
+ assert mp.hints is not None
+ assert mp.hints[0].name == "claude-3-sonnet"
+
+ def test_parse_model_preferences_list(self, context):
+ mp = context._parse_model_preferences(["claude-3-sonnet", "claude"])
+ assert isinstance(mp, ModelPreferences)
+ assert mp.hints is not None
+ assert [h.name for h in mp.hints] == ["claude-3-sonnet", "claude"]
+
+ def test_parse_model_preferences_object(self, context):
+ obj = ModelPreferences(hints=[])
+ assert context._parse_model_preferences(obj) is obj
+
+ def test_parse_model_preferences_invalid_type(self, context):
+ with pytest.raises(ValueError):
+ context._parse_model_preferences(123)
diff --git a/tests/utilities/test_mcp_config.py b/tests/utilities/test_mcp_config.py
index 627a149f1..b7737da1d 100644
--- a/tests/utilities/test_mcp_config.py
+++ b/tests/utilities/test_mcp_config.py
@@ -9,7 +9,7 @@ from fastmcp.client.transports import (
StdioTransport,
StreamableHttpTransport,
)
-from fastmcp.utilities.mcp_config import LocalMCPServer, MCPConfig, RemoteMCPServer
+from fastmcp.utilities.mcp_config import MCPConfig, RemoteMCPServer, StdioMCPServer
def test_parse_single_stdio_config():
@@ -89,7 +89,7 @@ def test_parse_multiple_servers():
assert isinstance(mcp_config.mcpServers["test_server"], RemoteMCPServer)
assert isinstance(mcp_config.mcpServers["test_server"].to_transport(), SSETransport)
- assert isinstance(mcp_config.mcpServers["test_server_2"], LocalMCPServer)
+ assert isinstance(mcp_config.mcpServers["test_server_2"], StdioMCPServer)
assert isinstance(
mcp_config.mcpServers["test_server_2"].to_transport(), StdioTransport
)