Spaces:
Running
Running
Merge branch 'main' into custom-routes
Browse files- docs/clients/client.mdx +1 -1
- docs/clients/transports.mdx +1 -1
- docs/servers/composition.mdx +1 -1
- docs/servers/context.mdx +4 -3
- docs/servers/proxy.mdx +1 -1
- src/fastmcp/client/client.py +17 -0
- src/fastmcp/client/transports.py +5 -1
- src/fastmcp/server/context.py +46 -0
- src/fastmcp/server/server.py +6 -6
- src/fastmcp/utilities/mcp_config.py +4 -3
- tests/client/test_client.py +12 -0
- tests/server/test_context.py +29 -0
- tests/utilities/test_mcp_config.py +2 -2
docs/clients/client.mdx
CHANGED
|
@@ -91,7 +91,7 @@ For more control over connection details (like headers for SSE, environment vari
|
|
| 91 |
|
| 92 |
### Multi-Server Clients
|
| 93 |
|
| 94 |
-
<VersionBadge version="2.
|
| 95 |
|
| 96 |
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.
|
| 97 |
|
|
|
|
| 91 |
|
| 92 |
### Multi-Server Clients
|
| 93 |
|
| 94 |
+
<VersionBadge version="2.4.0" />
|
| 95 |
|
| 96 |
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.
|
| 97 |
|
docs/clients/transports.mdx
CHANGED
|
@@ -323,7 +323,7 @@ Communication happens through efficient in-memory queues, making it very fast an
|
|
| 323 |
|
| 324 |
### MCPConfig Transport
|
| 325 |
|
| 326 |
-
<VersionBadge version="2.
|
| 327 |
|
| 328 |
- **Class:** `fastmcp.client.transports.MCPConfigTransport`
|
| 329 |
- **Inferred From:** An instance of `MCPConfig` or a dictionary matching the MCPConfig schema
|
|
|
|
| 323 |
|
| 324 |
### MCPConfig Transport
|
| 325 |
|
| 326 |
+
<VersionBadge version="2.4.0" />
|
| 327 |
|
| 328 |
- **Class:** `fastmcp.client.transports.MCPConfigTransport`
|
| 329 |
- **Inferred From:** An instance of `MCPConfig` or a dictionary matching the MCPConfig schema
|
docs/servers/composition.mdx
CHANGED
|
@@ -35,7 +35,7 @@ The choice of importing or mounting depends on your use case and requirements.
|
|
| 35 |
|
| 36 |
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.
|
| 37 |
|
| 38 |
-
<VersionBadge version="2.
|
| 39 |
|
| 40 |
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.
|
| 41 |
|
|
|
|
| 35 |
|
| 36 |
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.
|
| 37 |
|
| 38 |
+
<VersionBadge version="2.4.0" />
|
| 39 |
|
| 40 |
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.
|
| 41 |
|
docs/servers/context.mdx
CHANGED
|
@@ -228,8 +228,8 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
|
|
| 228 |
# Create a sampling prompt asking for sentiment analysis
|
| 229 |
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}"
|
| 230 |
|
| 231 |
-
# Send the sampling request to the client's LLM
|
| 232 |
-
response = await ctx.sample(prompt)
|
| 233 |
|
| 234 |
# Process the LLM's response
|
| 235 |
sentiment = response.text.strip().lower()
|
|
@@ -247,11 +247,12 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
|
|
| 247 |
|
| 248 |
**Method signature:**
|
| 249 |
|
| 250 |
-
- **`ctx.sample(messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None) -> TextContent | ImageContent`**
|
| 251 |
- `messages`: A string or list of strings/message objects to send to the LLM
|
| 252 |
- `system_prompt`: Optional system prompt to guide the LLM's behavior
|
| 253 |
- `temperature`: Optional sampling temperature (controls randomness)
|
| 254 |
- `max_tokens`: Optional maximum number of tokens to generate (defaults to 512)
|
|
|
|
| 255 |
- Returns the LLM's response as TextContent or ImageContent
|
| 256 |
|
| 257 |
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.
|
|
|
|
| 228 |
# Create a sampling prompt asking for sentiment analysis
|
| 229 |
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}"
|
| 230 |
|
| 231 |
+
# Send the sampling request to the client's LLM (provide a hint for the model you want to use)
|
| 232 |
+
response = await ctx.sample(prompt, model_preferences="claude-3-sonnet")
|
| 233 |
|
| 234 |
# Process the LLM's response
|
| 235 |
sentiment = response.text.strip().lower()
|
|
|
|
| 247 |
|
| 248 |
**Method signature:**
|
| 249 |
|
| 250 |
+
- **`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`**
|
| 251 |
- `messages`: A string or list of strings/message objects to send to the LLM
|
| 252 |
- `system_prompt`: Optional system prompt to guide the LLM's behavior
|
| 253 |
- `temperature`: Optional sampling temperature (controls randomness)
|
| 254 |
- `max_tokens`: Optional maximum number of tokens to generate (defaults to 512)
|
| 255 |
+
- `model_preferences`: Optional model selection preferences (e.g., a model hint string, list of hints, or a ModelPreferences object)
|
| 256 |
- Returns the LLM's response as TextContent or ImageContent
|
| 257 |
|
| 258 |
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.
|
docs/servers/proxy.mdx
CHANGED
|
@@ -106,7 +106,7 @@ proxy = FastMCP.as_proxy(
|
|
| 106 |
|
| 107 |
### Configuration-Based Proxies
|
| 108 |
|
| 109 |
-
<VersionBadge version="2.
|
| 110 |
|
| 111 |
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.
|
| 112 |
|
|
|
|
| 106 |
|
| 107 |
### Configuration-Based Proxies
|
| 108 |
|
| 109 |
+
<VersionBadge version="2.4.0" />
|
| 110 |
|
| 111 |
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.
|
| 112 |
|
src/fastmcp/client/client.py
CHANGED
|
@@ -210,6 +210,23 @@ class Client:
|
|
| 210 |
result = await self.session.send_ping()
|
| 211 |
return isinstance(result, mcp.types.EmptyResult)
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
async def progress(
|
| 214 |
self,
|
| 215 |
progress_token: str | int,
|
|
|
|
| 210 |
result = await self.session.send_ping()
|
| 211 |
return isinstance(result, mcp.types.EmptyResult)
|
| 212 |
|
| 213 |
+
async def cancel(
|
| 214 |
+
self,
|
| 215 |
+
request_id: str | int,
|
| 216 |
+
reason: str | None = None,
|
| 217 |
+
) -> None:
|
| 218 |
+
"""Send a cancellation notification for an in-progress request."""
|
| 219 |
+
notification = mcp.types.ClientNotification(
|
| 220 |
+
mcp.types.CancelledNotification(
|
| 221 |
+
method="notifications/cancelled",
|
| 222 |
+
params=mcp.types.CancelledNotificationParams(
|
| 223 |
+
requestId=request_id,
|
| 224 |
+
reason=reason,
|
| 225 |
+
),
|
| 226 |
+
)
|
| 227 |
+
)
|
| 228 |
+
await self.session.send_notification(notification)
|
| 229 |
+
|
| 230 |
async def progress(
|
| 231 |
self,
|
| 232 |
progress_token: str | int,
|
src/fastmcp/client/transports.py
CHANGED
|
@@ -535,8 +535,12 @@ class MCPConfigTransport(ClientTransport):
|
|
| 535 |
config = MCPConfig.from_dict(config)
|
| 536 |
self.config = config
|
| 537 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
# if there's exactly one server, create a client for that server
|
| 539 |
-
|
| 540 |
self.transport = list(self.config.mcpServers.values())[0].to_transport()
|
| 541 |
|
| 542 |
# otherwise create a composite client
|
|
|
|
| 535 |
config = MCPConfig.from_dict(config)
|
| 536 |
self.config = config
|
| 537 |
|
| 538 |
+
# if there are no servers, raise an error
|
| 539 |
+
if len(self.config.mcpServers) == 0:
|
| 540 |
+
raise ValueError("No MCP servers defined in the config")
|
| 541 |
+
|
| 542 |
# if there's exactly one server, create a client for that server
|
| 543 |
+
elif len(self.config.mcpServers) == 1:
|
| 544 |
self.transport = list(self.config.mcpServers.values())[0].to_transport()
|
| 545 |
|
| 546 |
# otherwise create a composite client
|
src/fastmcp/server/context.py
CHANGED
|
@@ -12,6 +12,8 @@ from mcp.shared.context import RequestContext
|
|
| 12 |
from mcp.types import (
|
| 13 |
CreateMessageResult,
|
| 14 |
ImageContent,
|
|
|
|
|
|
|
| 15 |
Root,
|
| 16 |
SamplingMessage,
|
| 17 |
TextContent,
|
|
@@ -200,6 +202,7 @@ class Context:
|
|
| 200 |
system_prompt: str | None = None,
|
| 201 |
temperature: float | None = None,
|
| 202 |
max_tokens: int | None = None,
|
|
|
|
| 203 |
) -> TextContent | ImageContent:
|
| 204 |
"""
|
| 205 |
Send a sampling request to the client and await the response.
|
|
@@ -231,6 +234,7 @@ class Context:
|
|
| 231 |
system_prompt=system_prompt,
|
| 232 |
temperature=temperature,
|
| 233 |
max_tokens=max_tokens,
|
|
|
|
| 234 |
)
|
| 235 |
|
| 236 |
return result.content
|
|
@@ -248,3 +252,45 @@ class Context:
|
|
| 248 |
)
|
| 249 |
|
| 250 |
return fastmcp.server.dependencies.get_http_request()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
from mcp.types import (
|
| 13 |
CreateMessageResult,
|
| 14 |
ImageContent,
|
| 15 |
+
ModelHint,
|
| 16 |
+
ModelPreferences,
|
| 17 |
Root,
|
| 18 |
SamplingMessage,
|
| 19 |
TextContent,
|
|
|
|
| 202 |
system_prompt: str | None = None,
|
| 203 |
temperature: float | None = None,
|
| 204 |
max_tokens: int | None = None,
|
| 205 |
+
model_preferences: ModelPreferences | str | list[str] | None = None,
|
| 206 |
) -> TextContent | ImageContent:
|
| 207 |
"""
|
| 208 |
Send a sampling request to the client and await the response.
|
|
|
|
| 234 |
system_prompt=system_prompt,
|
| 235 |
temperature=temperature,
|
| 236 |
max_tokens=max_tokens,
|
| 237 |
+
model_preferences=self._parse_model_preferences(model_preferences),
|
| 238 |
)
|
| 239 |
|
| 240 |
return result.content
|
|
|
|
| 252 |
)
|
| 253 |
|
| 254 |
return fastmcp.server.dependencies.get_http_request()
|
| 255 |
+
|
| 256 |
+
def _parse_model_preferences(
|
| 257 |
+
self, model_preferences: ModelPreferences | str | list[str] | None
|
| 258 |
+
) -> ModelPreferences | None:
|
| 259 |
+
"""
|
| 260 |
+
Validates and converts user input for model_preferences into a ModelPreferences object.
|
| 261 |
+
|
| 262 |
+
Args:
|
| 263 |
+
model_preferences (ModelPreferences | str | list[str] | None):
|
| 264 |
+
The model preferences to use. Accepts:
|
| 265 |
+
- ModelPreferences (returns as-is)
|
| 266 |
+
- str (single model hint)
|
| 267 |
+
- list[str] (multiple model hints)
|
| 268 |
+
- None (no preferences)
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
|
| 272 |
+
|
| 273 |
+
Raises:
|
| 274 |
+
ValueError: If the input is not a supported type or contains invalid values.
|
| 275 |
+
"""
|
| 276 |
+
if model_preferences is None:
|
| 277 |
+
return None
|
| 278 |
+
elif isinstance(model_preferences, ModelPreferences):
|
| 279 |
+
return model_preferences
|
| 280 |
+
elif isinstance(model_preferences, str):
|
| 281 |
+
# Single model hint
|
| 282 |
+
return ModelPreferences(hints=[ModelHint(name=model_preferences)])
|
| 283 |
+
elif isinstance(model_preferences, list):
|
| 284 |
+
# List of model hints (strings)
|
| 285 |
+
if not all(isinstance(h, str) for h in model_preferences):
|
| 286 |
+
raise ValueError(
|
| 287 |
+
"All elements of model_preferences list must be"
|
| 288 |
+
" strings (model name hints)."
|
| 289 |
+
)
|
| 290 |
+
return ModelPreferences(
|
| 291 |
+
hints=[ModelHint(name=h) for h in model_preferences]
|
| 292 |
+
)
|
| 293 |
+
else:
|
| 294 |
+
raise ValueError(
|
| 295 |
+
"model_preferences must be one of: ModelPreferences, str, list[str], or None."
|
| 296 |
+
)
|
src/fastmcp/server/server.py
CHANGED
|
@@ -998,7 +998,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 998 |
from fastmcp.server.proxy import FastMCPProxy
|
| 999 |
|
| 1000 |
if tool_separator is not None:
|
| 1001 |
-
# Deprecated since 2.
|
| 1002 |
warnings.warn(
|
| 1003 |
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
| 1004 |
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
@@ -1007,7 +1007,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1007 |
)
|
| 1008 |
|
| 1009 |
if resource_separator is not None:
|
| 1010 |
-
# Deprecated since 2.
|
| 1011 |
warnings.warn(
|
| 1012 |
"The resource_separator parameter is deprecated and ignored. "
|
| 1013 |
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
@@ -1016,7 +1016,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1016 |
)
|
| 1017 |
|
| 1018 |
if prompt_separator is not None:
|
| 1019 |
-
# Deprecated since 2.
|
| 1020 |
warnings.warn(
|
| 1021 |
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
| 1022 |
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
@@ -1083,7 +1083,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1083 |
prompt_separator: Deprecated. Separator for prompt names.
|
| 1084 |
"""
|
| 1085 |
if tool_separator is not None:
|
| 1086 |
-
# Deprecated since 2.
|
| 1087 |
warnings.warn(
|
| 1088 |
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
| 1089 |
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
@@ -1092,7 +1092,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1092 |
)
|
| 1093 |
|
| 1094 |
if resource_separator is not None:
|
| 1095 |
-
# Deprecated since 2.
|
| 1096 |
warnings.warn(
|
| 1097 |
"The resource_separator parameter is deprecated and ignored. "
|
| 1098 |
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
@@ -1101,7 +1101,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1101 |
)
|
| 1102 |
|
| 1103 |
if prompt_separator is not None:
|
| 1104 |
-
# Deprecated since 2.
|
| 1105 |
warnings.warn(
|
| 1106 |
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
| 1107 |
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
|
|
| 998 |
from fastmcp.server.proxy import FastMCPProxy
|
| 999 |
|
| 1000 |
if tool_separator is not None:
|
| 1001 |
+
# Deprecated since 2.4.0
|
| 1002 |
warnings.warn(
|
| 1003 |
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
| 1004 |
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
|
|
| 1007 |
)
|
| 1008 |
|
| 1009 |
if resource_separator is not None:
|
| 1010 |
+
# Deprecated since 2.4.0
|
| 1011 |
warnings.warn(
|
| 1012 |
"The resource_separator parameter is deprecated and ignored. "
|
| 1013 |
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
|
|
| 1016 |
)
|
| 1017 |
|
| 1018 |
if prompt_separator is not None:
|
| 1019 |
+
# Deprecated since 2.4.0
|
| 1020 |
warnings.warn(
|
| 1021 |
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
| 1022 |
"Prompts are now prefixed using 'prefix_promptname' format.",
|
|
|
|
| 1083 |
prompt_separator: Deprecated. Separator for prompt names.
|
| 1084 |
"""
|
| 1085 |
if tool_separator is not None:
|
| 1086 |
+
# Deprecated since 2.4.0
|
| 1087 |
warnings.warn(
|
| 1088 |
"The tool_separator parameter is deprecated and will be removed in a future version. "
|
| 1089 |
"Tools are now prefixed using 'prefix_toolname' format.",
|
|
|
|
| 1092 |
)
|
| 1093 |
|
| 1094 |
if resource_separator is not None:
|
| 1095 |
+
# Deprecated since 2.4.0
|
| 1096 |
warnings.warn(
|
| 1097 |
"The resource_separator parameter is deprecated and ignored. "
|
| 1098 |
"Resource prefixes are now added using the protocol://prefix/path format.",
|
|
|
|
| 1101 |
)
|
| 1102 |
|
| 1103 |
if prompt_separator is not None:
|
| 1104 |
+
# Deprecated since 2.4.0
|
| 1105 |
warnings.warn(
|
| 1106 |
"The prompt_separator parameter is deprecated and will be removed in a future version. "
|
| 1107 |
"Prompts are now prefixed using 'prefix_promptname' format.",
|
src/fastmcp/utilities/mcp_config.py
CHANGED
|
@@ -32,11 +32,12 @@ def infer_transport_type_from_url(
|
|
| 32 |
return "streamable-http"
|
| 33 |
|
| 34 |
|
| 35 |
-
class
|
| 36 |
command: str
|
| 37 |
args: list[str] = Field(default_factory=list)
|
| 38 |
env: dict[str, Any] = Field(default_factory=dict)
|
| 39 |
cwd: str | None = None
|
|
|
|
| 40 |
|
| 41 |
def to_transport(self) -> StdioTransport:
|
| 42 |
from fastmcp.client.transports import StdioTransport
|
|
@@ -51,8 +52,8 @@ class LocalMCPServer(BaseModel):
|
|
| 51 |
|
| 52 |
class RemoteMCPServer(BaseModel):
|
| 53 |
url: str
|
| 54 |
-
transport: Literal["streamable-http", "sse", "http"] | None = None
|
| 55 |
headers: dict[str, str] = Field(default_factory=dict)
|
|
|
|
| 56 |
|
| 57 |
def to_transport(self) -> StreamableHttpTransport | SSETransport:
|
| 58 |
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
|
@@ -69,7 +70,7 @@ class RemoteMCPServer(BaseModel):
|
|
| 69 |
|
| 70 |
|
| 71 |
class MCPConfig(BaseModel):
|
| 72 |
-
mcpServers: dict[str,
|
| 73 |
|
| 74 |
@classmethod
|
| 75 |
def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
|
|
|
|
| 32 |
return "streamable-http"
|
| 33 |
|
| 34 |
|
| 35 |
+
class StdioMCPServer(BaseModel):
|
| 36 |
command: str
|
| 37 |
args: list[str] = Field(default_factory=list)
|
| 38 |
env: dict[str, Any] = Field(default_factory=dict)
|
| 39 |
cwd: str | None = None
|
| 40 |
+
transport: Literal["stdio"] = "stdio"
|
| 41 |
|
| 42 |
def to_transport(self) -> StdioTransport:
|
| 43 |
from fastmcp.client.transports import StdioTransport
|
|
|
|
| 52 |
|
| 53 |
class RemoteMCPServer(BaseModel):
|
| 54 |
url: str
|
|
|
|
| 55 |
headers: dict[str, str] = Field(default_factory=dict)
|
| 56 |
+
transport: Literal["streamable-http", "sse", "http"] | None = None
|
| 57 |
|
| 58 |
def to_transport(self) -> StreamableHttpTransport | SSETransport:
|
| 59 |
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
|
|
|
| 70 |
|
| 71 |
|
| 72 |
class MCPConfig(BaseModel):
|
| 73 |
+
mcpServers: dict[str, StdioMCPServer | RemoteMCPServer]
|
| 74 |
|
| 75 |
@classmethod
|
| 76 |
def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
|
tests/client/test_client.py
CHANGED
|
@@ -673,6 +673,18 @@ class TestInferTransport:
|
|
| 673 |
assert transport.transport.command == "echo"
|
| 674 |
assert transport.transport.args == ["hello"]
|
| 675 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
def test_infer_composite_client(self):
|
| 677 |
config = {
|
| 678 |
"mcpServers": {
|
|
|
|
| 673 |
assert transport.transport.command == "echo"
|
| 674 |
assert transport.transport.args == ["hello"]
|
| 675 |
|
| 676 |
+
def test_config_with_no_servers(self):
|
| 677 |
+
"""Test that an empty MCPConfig raises a ValueError."""
|
| 678 |
+
config = {"mcpServers": {}}
|
| 679 |
+
with pytest.raises(ValueError, match="No MCP servers defined in the config"):
|
| 680 |
+
infer_transport(config)
|
| 681 |
+
|
| 682 |
+
def test_mcpconfigtransport_with_no_servers(self):
|
| 683 |
+
"""Test that MCPConfigTransport raises a ValueError when initialized with an empty config."""
|
| 684 |
+
config = {"mcpServers": {}}
|
| 685 |
+
with pytest.raises(ValueError, match="No MCP servers defined in the config"):
|
| 686 |
+
MCPConfigTransport(config=config)
|
| 687 |
+
|
| 688 |
def test_infer_composite_client(self):
|
| 689 |
config = {
|
| 690 |
"mcpServers": {
|
tests/server/test_context.py
CHANGED
|
@@ -2,9 +2,11 @@ import warnings
|
|
| 2 |
from unittest.mock import MagicMock, patch
|
| 3 |
|
| 4 |
import pytest
|
|
|
|
| 5 |
from starlette.requests import Request
|
| 6 |
|
| 7 |
from fastmcp.server.context import Context
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
class TestContextDeprecations:
|
|
@@ -57,3 +59,30 @@ class TestContextDeprecations:
|
|
| 57 |
assert "https://gofastmcp.com/patterns/http-requests" in str(
|
| 58 |
warning.message
|
| 59 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from unittest.mock import MagicMock, patch
|
| 3 |
|
| 4 |
import pytest
|
| 5 |
+
from mcp.types import ModelPreferences
|
| 6 |
from starlette.requests import Request
|
| 7 |
|
| 8 |
from fastmcp.server.context import Context
|
| 9 |
+
from fastmcp.server.server import FastMCP
|
| 10 |
|
| 11 |
|
| 12 |
class TestContextDeprecations:
|
|
|
|
| 59 |
assert "https://gofastmcp.com/patterns/http-requests" in str(
|
| 60 |
warning.message
|
| 61 |
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@pytest.fixture
|
| 65 |
+
def context():
|
| 66 |
+
return Context(fastmcp=FastMCP())
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class TestParseModelPreferences:
|
| 70 |
+
def test_parse_model_preferences_string(self, context):
|
| 71 |
+
mp = context._parse_model_preferences("claude-3-sonnet")
|
| 72 |
+
assert isinstance(mp, ModelPreferences)
|
| 73 |
+
assert mp.hints is not None
|
| 74 |
+
assert mp.hints[0].name == "claude-3-sonnet"
|
| 75 |
+
|
| 76 |
+
def test_parse_model_preferences_list(self, context):
|
| 77 |
+
mp = context._parse_model_preferences(["claude-3-sonnet", "claude"])
|
| 78 |
+
assert isinstance(mp, ModelPreferences)
|
| 79 |
+
assert mp.hints is not None
|
| 80 |
+
assert [h.name for h in mp.hints] == ["claude-3-sonnet", "claude"]
|
| 81 |
+
|
| 82 |
+
def test_parse_model_preferences_object(self, context):
|
| 83 |
+
obj = ModelPreferences(hints=[])
|
| 84 |
+
assert context._parse_model_preferences(obj) is obj
|
| 85 |
+
|
| 86 |
+
def test_parse_model_preferences_invalid_type(self, context):
|
| 87 |
+
with pytest.raises(ValueError):
|
| 88 |
+
context._parse_model_preferences(123)
|
tests/utilities/test_mcp_config.py
CHANGED
|
@@ -9,7 +9,7 @@ from fastmcp.client.transports import (
|
|
| 9 |
StdioTransport,
|
| 10 |
StreamableHttpTransport,
|
| 11 |
)
|
| 12 |
-
from fastmcp.utilities.mcp_config import
|
| 13 |
|
| 14 |
|
| 15 |
def test_parse_single_stdio_config():
|
|
@@ -89,7 +89,7 @@ def test_parse_multiple_servers():
|
|
| 89 |
assert isinstance(mcp_config.mcpServers["test_server"], RemoteMCPServer)
|
| 90 |
assert isinstance(mcp_config.mcpServers["test_server"].to_transport(), SSETransport)
|
| 91 |
|
| 92 |
-
assert isinstance(mcp_config.mcpServers["test_server_2"],
|
| 93 |
assert isinstance(
|
| 94 |
mcp_config.mcpServers["test_server_2"].to_transport(), StdioTransport
|
| 95 |
)
|
|
|
|
| 9 |
StdioTransport,
|
| 10 |
StreamableHttpTransport,
|
| 11 |
)
|
| 12 |
+
from fastmcp.utilities.mcp_config import MCPConfig, RemoteMCPServer, StdioMCPServer
|
| 13 |
|
| 14 |
|
| 15 |
def test_parse_single_stdio_config():
|
|
|
|
| 89 |
assert isinstance(mcp_config.mcpServers["test_server"], RemoteMCPServer)
|
| 90 |
assert isinstance(mcp_config.mcpServers["test_server"].to_transport(), SSETransport)
|
| 91 |
|
| 92 |
+
assert isinstance(mcp_config.mcpServers["test_server_2"], StdioMCPServer)
|
| 93 |
assert isinstance(
|
| 94 |
mcp_config.mcpServers["test_server_2"].to_transport(), StdioTransport
|
| 95 |
)
|