fastmcp 2.4.0__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/client/client.py +23 -1
- fastmcp/client/transports.py +68 -18
- fastmcp/prompts/prompt.py +11 -4
- fastmcp/prompts/prompt_manager.py +25 -5
- fastmcp/resources/resource_manager.py +31 -5
- fastmcp/resources/template.py +10 -5
- fastmcp/server/context.py +46 -0
- fastmcp/server/http.py +2 -0
- fastmcp/server/openapi.py +324 -64
- fastmcp/server/server.py +101 -49
- fastmcp/settings.py +30 -1
- fastmcp/tools/tool.py +5 -1
- fastmcp/tools/tool_manager.py +9 -2
- fastmcp/utilities/logging.py +6 -1
- fastmcp/utilities/mcp_config.py +4 -3
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.dist-info}/METADATA +4 -4
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.dist-info}/RECORD +20 -20
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/client.py
CHANGED
|
@@ -210,6 +210,23 @@ class Client:
|
|
|
210
210
|
result = await self.session.send_ping()
|
|
211
211
|
return isinstance(result, mcp.types.EmptyResult)
|
|
212
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
|
+
|
|
213
230
|
async def progress(
|
|
214
231
|
self,
|
|
215
232
|
progress_token: str | int,
|
|
@@ -322,7 +339,12 @@ class Client:
|
|
|
322
339
|
RuntimeError: If called while the client is not connected.
|
|
323
340
|
"""
|
|
324
341
|
if isinstance(uri, str):
|
|
325
|
-
|
|
342
|
+
try:
|
|
343
|
+
uri = AnyUrl(uri) # Ensure AnyUrl
|
|
344
|
+
except Exception as e:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"Provided resource URI is invalid: {str(uri)!r}"
|
|
347
|
+
) from e
|
|
326
348
|
result = await self.read_resource_mcp(uri)
|
|
327
349
|
return result.contents
|
|
328
350
|
|
fastmcp/client/transports.py
CHANGED
|
@@ -19,11 +19,13 @@ from mcp.client.sse import sse_client
|
|
|
19
19
|
from mcp.client.stdio import stdio_client
|
|
20
20
|
from mcp.client.streamable_http import streamablehttp_client
|
|
21
21
|
from mcp.client.websocket import websocket_client
|
|
22
|
+
from mcp.server.fastmcp import FastMCP as FastMCP1Server
|
|
22
23
|
from mcp.shared.memory import create_connected_server_and_client_session
|
|
23
24
|
from pydantic import AnyUrl
|
|
24
25
|
from typing_extensions import Unpack
|
|
25
26
|
|
|
26
27
|
from fastmcp.server import FastMCP as FastMCPServer
|
|
28
|
+
from fastmcp.server.dependencies import get_http_request
|
|
27
29
|
from fastmcp.server.server import FastMCP
|
|
28
30
|
from fastmcp.utilities.logging import get_logger
|
|
29
31
|
from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
|
|
@@ -33,6 +35,11 @@ if TYPE_CHECKING:
|
|
|
33
35
|
|
|
34
36
|
logger = get_logger(__name__)
|
|
35
37
|
|
|
38
|
+
# these headers, when forwarded to the remote server, can cause issues
|
|
39
|
+
EXCLUDE_HEADERS = {
|
|
40
|
+
"content-length",
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
|
|
37
44
|
class SessionKwargs(TypedDict, total=False):
|
|
38
45
|
"""Keyword arguments for the MCP ClientSession constructor."""
|
|
@@ -131,7 +138,24 @@ class SSETransport(ClientTransport):
|
|
|
131
138
|
async def connect_session(
|
|
132
139
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
133
140
|
) -> AsyncIterator[ClientSession]:
|
|
134
|
-
client_kwargs = {
|
|
141
|
+
client_kwargs: dict[str, Any] = {
|
|
142
|
+
"headers": self.headers,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# load headers from an active HTTP request, if available. This will only be true
|
|
146
|
+
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
|
|
147
|
+
# need to be forwarded to the remote server.
|
|
148
|
+
try:
|
|
149
|
+
active_request = get_http_request()
|
|
150
|
+
for name, value in active_request.headers.items():
|
|
151
|
+
name = name.lower()
|
|
152
|
+
if name not in self.headers and name not in {
|
|
153
|
+
h.lower() for h in EXCLUDE_HEADERS
|
|
154
|
+
}:
|
|
155
|
+
client_kwargs["headers"][name] = str(value)
|
|
156
|
+
except RuntimeError:
|
|
157
|
+
client_kwargs["headers"] = self.headers
|
|
158
|
+
|
|
135
159
|
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
|
136
160
|
# instead we simply leave the kwarg out if it's not provided
|
|
137
161
|
if self.sse_read_timeout is not None:
|
|
@@ -142,9 +166,7 @@ class SSETransport(ClientTransport):
|
|
|
142
166
|
)
|
|
143
167
|
client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
|
|
144
168
|
|
|
145
|
-
async with sse_client(
|
|
146
|
-
self.url, headers=self.headers, **client_kwargs
|
|
147
|
-
) as transport:
|
|
169
|
+
async with sse_client(self.url, **client_kwargs) as transport:
|
|
148
170
|
read_stream, write_stream = transport
|
|
149
171
|
async with ClientSession(
|
|
150
172
|
read_stream, write_stream, **session_kwargs
|
|
@@ -179,7 +201,26 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
179
201
|
async def connect_session(
|
|
180
202
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
181
203
|
) -> AsyncIterator[ClientSession]:
|
|
182
|
-
client_kwargs = {
|
|
204
|
+
client_kwargs: dict[str, Any] = {
|
|
205
|
+
"headers": self.headers,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# load headers from an active HTTP request, if available. This will only be true
|
|
209
|
+
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
|
|
210
|
+
# need to be forwarded to the remote server.
|
|
211
|
+
try:
|
|
212
|
+
active_request = get_http_request()
|
|
213
|
+
for name, value in active_request.headers.items():
|
|
214
|
+
name = name.lower()
|
|
215
|
+
if name not in self.headers and name not in {
|
|
216
|
+
h.lower() for h in EXCLUDE_HEADERS
|
|
217
|
+
}:
|
|
218
|
+
client_kwargs["headers"][name] = str(value)
|
|
219
|
+
|
|
220
|
+
except RuntimeError:
|
|
221
|
+
client_kwargs["headers"] = self.headers
|
|
222
|
+
print(client_kwargs)
|
|
223
|
+
|
|
183
224
|
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
|
184
225
|
# instead we simply leave the kwarg out if it's not provided
|
|
185
226
|
if self.sse_read_timeout is not None:
|
|
@@ -187,9 +228,7 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
187
228
|
if session_kwargs.get("read_timeout_seconds", None) is not None:
|
|
188
229
|
client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
|
|
189
230
|
|
|
190
|
-
async with streamablehttp_client(
|
|
191
|
-
self.url, headers=self.headers, **client_kwargs
|
|
192
|
-
) as transport:
|
|
231
|
+
async with streamablehttp_client(self.url, **client_kwargs) as transport:
|
|
193
232
|
read_stream, write_stream, _ = transport
|
|
194
233
|
async with ClientSession(
|
|
195
234
|
read_stream, write_stream, **session_kwargs
|
|
@@ -448,15 +487,21 @@ class NpxStdioTransport(StdioTransport):
|
|
|
448
487
|
|
|
449
488
|
|
|
450
489
|
class FastMCPTransport(ClientTransport):
|
|
451
|
-
"""
|
|
452
|
-
Special transport for in-memory connections to an MCP server.
|
|
490
|
+
"""In-memory transport for FastMCP servers.
|
|
453
491
|
|
|
454
|
-
This
|
|
455
|
-
|
|
492
|
+
This transport connects directly to a FastMCP server instance in the same
|
|
493
|
+
Python process. It works with both FastMCP 2.x servers and FastMCP 1.0
|
|
494
|
+
servers from the low-level MCP SDK. This is particularly useful for unit
|
|
495
|
+
tests or scenarios where client and server run in the same runtime.
|
|
456
496
|
"""
|
|
457
497
|
|
|
458
|
-
def __init__(self, mcp: FastMCPServer):
|
|
459
|
-
|
|
498
|
+
def __init__(self, mcp: FastMCPServer | FastMCP1Server):
|
|
499
|
+
"""Initialize a FastMCPTransport from a FastMCP server instance."""
|
|
500
|
+
|
|
501
|
+
# Accept both FastMCP 2.x and FastMCP 1.0 servers. Both expose a
|
|
502
|
+
# ``_mcp_server`` attribute pointing to the underlying MCP server
|
|
503
|
+
# implementation, so we can treat them identically.
|
|
504
|
+
self.server = mcp
|
|
460
505
|
|
|
461
506
|
@contextlib.asynccontextmanager
|
|
462
507
|
async def connect_session(
|
|
@@ -528,8 +573,12 @@ class MCPConfigTransport(ClientTransport):
|
|
|
528
573
|
config = MCPConfig.from_dict(config)
|
|
529
574
|
self.config = config
|
|
530
575
|
|
|
576
|
+
# if there are no servers, raise an error
|
|
577
|
+
if len(self.config.mcpServers) == 0:
|
|
578
|
+
raise ValueError("No MCP servers defined in the config")
|
|
579
|
+
|
|
531
580
|
# if there's exactly one server, create a client for that server
|
|
532
|
-
|
|
581
|
+
elif len(self.config.mcpServers) == 1:
|
|
533
582
|
self.transport = list(self.config.mcpServers.values())[0].to_transport()
|
|
534
583
|
|
|
535
584
|
# otherwise create a composite client
|
|
@@ -558,6 +607,7 @@ class MCPConfigTransport(ClientTransport):
|
|
|
558
607
|
def infer_transport(
|
|
559
608
|
transport: ClientTransport
|
|
560
609
|
| FastMCPServer
|
|
610
|
+
| FastMCP1Server
|
|
561
611
|
| AnyUrl
|
|
562
612
|
| Path
|
|
563
613
|
| MCPConfig
|
|
@@ -573,7 +623,7 @@ def infer_transport(
|
|
|
573
623
|
|
|
574
624
|
The function supports these input types:
|
|
575
625
|
- ClientTransport: Used directly without modification
|
|
576
|
-
- FastMCPServer: Creates an in-memory FastMCPTransport
|
|
626
|
+
- FastMCPServer or FastMCP1Server: Creates an in-memory FastMCPTransport
|
|
577
627
|
- Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
|
|
578
628
|
- AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
|
|
579
629
|
- MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
|
|
@@ -610,8 +660,8 @@ def infer_transport(
|
|
|
610
660
|
if isinstance(transport, ClientTransport):
|
|
611
661
|
return transport
|
|
612
662
|
|
|
613
|
-
# the transport is a FastMCP server
|
|
614
|
-
elif isinstance(transport, FastMCPServer):
|
|
663
|
+
# the transport is a FastMCP server (2.x or 1.0)
|
|
664
|
+
elif isinstance(transport, FastMCPServer | FastMCP1Server):
|
|
615
665
|
inferred_transport = FastMCPTransport(mcp=transport)
|
|
616
666
|
|
|
617
667
|
# the transport is a path to a script
|
fastmcp/prompts/prompt.py
CHANGED
|
@@ -12,6 +12,7 @@ from mcp.types import Prompt as MCPPrompt
|
|
|
12
12
|
from mcp.types import PromptArgument as MCPPromptArgument
|
|
13
13
|
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
|
|
14
14
|
|
|
15
|
+
from fastmcp.exceptions import PromptError
|
|
15
16
|
from fastmcp.server.dependencies import get_context
|
|
16
17
|
from fastmcp.utilities.json_schema import compress_schema
|
|
17
18
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -96,7 +97,7 @@ class Prompt(BaseModel):
|
|
|
96
97
|
"""
|
|
97
98
|
from fastmcp.server.context import Context
|
|
98
99
|
|
|
99
|
-
func_name = name or fn.__name__
|
|
100
|
+
func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
100
101
|
|
|
101
102
|
if func_name == "<lambda>":
|
|
102
103
|
raise ValueError("You must provide a name for lambda functions")
|
|
@@ -108,6 +109,12 @@ class Prompt(BaseModel):
|
|
|
108
109
|
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
109
110
|
raise ValueError("Functions with **kwargs are not supported as prompts")
|
|
110
111
|
|
|
112
|
+
description = description or fn.__doc__
|
|
113
|
+
|
|
114
|
+
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
115
|
+
if not inspect.isroutine(fn):
|
|
116
|
+
fn = fn.__call__
|
|
117
|
+
|
|
111
118
|
type_adapter = get_cached_typeadapter(fn)
|
|
112
119
|
parameters = type_adapter.json_schema()
|
|
113
120
|
|
|
@@ -138,7 +145,7 @@ class Prompt(BaseModel):
|
|
|
138
145
|
|
|
139
146
|
return cls(
|
|
140
147
|
name=func_name,
|
|
141
|
-
description=description
|
|
148
|
+
description=description,
|
|
142
149
|
arguments=arguments,
|
|
143
150
|
fn=fn,
|
|
144
151
|
tags=tags or set(),
|
|
@@ -199,12 +206,12 @@ class Prompt(BaseModel):
|
|
|
199
206
|
)
|
|
200
207
|
)
|
|
201
208
|
except Exception:
|
|
202
|
-
raise
|
|
209
|
+
raise PromptError("Could not convert prompt result to message.")
|
|
203
210
|
|
|
204
211
|
return messages
|
|
205
212
|
except Exception as e:
|
|
206
213
|
logger.exception(f"Error rendering prompt {self.name}: {e}")
|
|
207
|
-
raise
|
|
214
|
+
raise PromptError(f"Error rendering prompt {self.name}.")
|
|
208
215
|
|
|
209
216
|
def __eq__(self, other: object) -> bool:
|
|
210
217
|
if not isinstance(other, Prompt):
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
7
7
|
|
|
8
8
|
from mcp import GetPromptResult
|
|
9
9
|
|
|
10
|
-
from fastmcp.exceptions import NotFoundError
|
|
10
|
+
from fastmcp.exceptions import NotFoundError, PromptError
|
|
11
11
|
from fastmcp.prompts.prompt import Prompt, PromptResult
|
|
12
12
|
from fastmcp.settings import DuplicateBehavior
|
|
13
13
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -21,8 +21,13 @@ logger = get_logger(__name__)
|
|
|
21
21
|
class PromptManager:
|
|
22
22
|
"""Manages FastMCP prompts."""
|
|
23
23
|
|
|
24
|
-
def __init__(
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
duplicate_behavior: DuplicateBehavior | None = None,
|
|
27
|
+
mask_error_details: bool = False,
|
|
28
|
+
):
|
|
25
29
|
self._prompts: dict[str, Prompt] = {}
|
|
30
|
+
self.mask_error_details = mask_error_details
|
|
26
31
|
|
|
27
32
|
# Default to "warn" if None is provided
|
|
28
33
|
if duplicate_behavior is None:
|
|
@@ -85,9 +90,24 @@ class PromptManager:
|
|
|
85
90
|
if not prompt:
|
|
86
91
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
try:
|
|
94
|
+
messages = await prompt.render(arguments)
|
|
95
|
+
return GetPromptResult(description=prompt.description, messages=messages)
|
|
96
|
+
|
|
97
|
+
# Pass through PromptErrors as-is
|
|
98
|
+
except PromptError as e:
|
|
99
|
+
logger.exception(f"Error rendering prompt {name!r}: {e}")
|
|
100
|
+
raise e
|
|
101
|
+
|
|
102
|
+
# Handle other exceptions
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.exception(f"Error rendering prompt {name!r}: {e}")
|
|
105
|
+
if self.mask_error_details:
|
|
106
|
+
# Mask internal details
|
|
107
|
+
raise PromptError(f"Error rendering prompt {name!r}")
|
|
108
|
+
else:
|
|
109
|
+
# Include original error details
|
|
110
|
+
raise PromptError(f"Error rendering prompt {name!r}: {e}")
|
|
91
111
|
|
|
92
112
|
def has_prompt(self, key: str) -> bool:
|
|
93
113
|
"""Check if a prompt exists."""
|
|
@@ -22,9 +22,22 @@ logger = get_logger(__name__)
|
|
|
22
22
|
class ResourceManager:
|
|
23
23
|
"""Manages FastMCP resources."""
|
|
24
24
|
|
|
25
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
duplicate_behavior: DuplicateBehavior | None = None,
|
|
28
|
+
mask_error_details: bool = False,
|
|
29
|
+
):
|
|
30
|
+
"""Initialize the ResourceManager.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
duplicate_behavior: How to handle duplicate resources
|
|
34
|
+
(warn, error, replace, ignore)
|
|
35
|
+
mask_error_details: Whether to mask error details from exceptions
|
|
36
|
+
other than ResourceError
|
|
37
|
+
"""
|
|
26
38
|
self._resources: dict[str, Resource] = {}
|
|
27
39
|
self._templates: dict[str, ResourceTemplate] = {}
|
|
40
|
+
self.mask_error_details = mask_error_details
|
|
28
41
|
|
|
29
42
|
# Default to "warn" if None is provided
|
|
30
43
|
if duplicate_behavior is None:
|
|
@@ -35,7 +48,6 @@ class ResourceManager:
|
|
|
35
48
|
f"Invalid duplicate_behavior: {duplicate_behavior}. "
|
|
36
49
|
f"Must be one of: {', '.join(DuplicateBehavior.__args__)}"
|
|
37
50
|
)
|
|
38
|
-
|
|
39
51
|
self.duplicate_behavior = duplicate_behavior
|
|
40
52
|
|
|
41
53
|
def add_resource_or_template_from_fn(
|
|
@@ -244,12 +256,21 @@ class ResourceManager:
|
|
|
244
256
|
uri_str,
|
|
245
257
|
params=params,
|
|
246
258
|
)
|
|
259
|
+
# Pass through ResourceErrors as-is
|
|
247
260
|
except ResourceError as e:
|
|
248
261
|
logger.error(f"Error creating resource from template: {e}")
|
|
249
262
|
raise e
|
|
263
|
+
# Handle other exceptions
|
|
250
264
|
except Exception as e:
|
|
251
265
|
logger.error(f"Error creating resource from template: {e}")
|
|
252
|
-
|
|
266
|
+
if self.mask_error_details:
|
|
267
|
+
# Mask internal details
|
|
268
|
+
raise ValueError("Error creating resource from template") from e
|
|
269
|
+
else:
|
|
270
|
+
# Include original error details
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"Error creating resource from template: {e}"
|
|
273
|
+
) from e
|
|
253
274
|
|
|
254
275
|
raise NotFoundError(f"Unknown resource: {uri_str}")
|
|
255
276
|
|
|
@@ -265,10 +286,15 @@ class ResourceManager:
|
|
|
265
286
|
logger.error(f"Error reading resource {uri!r}: {e}")
|
|
266
287
|
raise e
|
|
267
288
|
|
|
268
|
-
#
|
|
289
|
+
# Handle other exceptions
|
|
269
290
|
except Exception as e:
|
|
270
291
|
logger.error(f"Error reading resource {uri!r}: {e}")
|
|
271
|
-
|
|
292
|
+
if self.mask_error_details:
|
|
293
|
+
# Mask internal details
|
|
294
|
+
raise ResourceError(f"Error reading resource {uri!r}") from e
|
|
295
|
+
else:
|
|
296
|
+
# Include original error details
|
|
297
|
+
raise ResourceError(f"Error reading resource {uri!r}: {e}") from e
|
|
272
298
|
|
|
273
299
|
def get_resources(self) -> dict[str, Resource]:
|
|
274
300
|
"""Get all registered resources, keyed by URI."""
|
fastmcp/resources/template.py
CHANGED
|
@@ -14,7 +14,6 @@ from pydantic import (
|
|
|
14
14
|
BaseModel,
|
|
15
15
|
BeforeValidator,
|
|
16
16
|
Field,
|
|
17
|
-
TypeAdapter,
|
|
18
17
|
field_validator,
|
|
19
18
|
validate_call,
|
|
20
19
|
)
|
|
@@ -25,6 +24,7 @@ from fastmcp.utilities.json_schema import compress_schema
|
|
|
25
24
|
from fastmcp.utilities.types import (
|
|
26
25
|
_convert_set_defaults,
|
|
27
26
|
find_kwarg_by_type,
|
|
27
|
+
get_cached_typeadapter,
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
|
|
@@ -97,7 +97,7 @@ class ResourceTemplate(BaseModel):
|
|
|
97
97
|
"""Create a template from a function."""
|
|
98
98
|
from fastmcp.server.context import Context
|
|
99
99
|
|
|
100
|
-
func_name = name or fn.__name__
|
|
100
|
+
func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
101
101
|
if func_name == "<lambda>":
|
|
102
102
|
raise ValueError("You must provide a name for lambda functions")
|
|
103
103
|
|
|
@@ -148,8 +148,13 @@ class ResourceTemplate(BaseModel):
|
|
|
148
148
|
f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
description = description or fn.__doc__ or ""
|
|
152
|
+
|
|
153
|
+
if not inspect.isroutine(fn):
|
|
154
|
+
fn = fn.__call__
|
|
155
|
+
|
|
156
|
+
type_adapter = get_cached_typeadapter(fn)
|
|
157
|
+
parameters = type_adapter.json_schema()
|
|
153
158
|
|
|
154
159
|
# compress the schema
|
|
155
160
|
prune_params = [context_kwarg] if context_kwarg else None
|
|
@@ -161,7 +166,7 @@ class ResourceTemplate(BaseModel):
|
|
|
161
166
|
return cls(
|
|
162
167
|
uri_template=uri_template,
|
|
163
168
|
name=func_name,
|
|
164
|
-
description=description
|
|
169
|
+
description=description,
|
|
165
170
|
mime_type=mime_type or "text/plain",
|
|
166
171
|
fn=fn,
|
|
167
172
|
parameters=parameters,
|
fastmcp/server/context.py
CHANGED
|
@@ -12,6 +12,8 @@ from mcp.shared.context import RequestContext
|
|
|
12
12
|
from mcp.types import (
|
|
13
13
|
CreateMessageResult,
|
|
14
14
|
ImageContent,
|
|
15
|
+
ModelHint,
|
|
16
|
+
ModelPreferences,
|
|
15
17
|
Root,
|
|
16
18
|
SamplingMessage,
|
|
17
19
|
TextContent,
|
|
@@ -200,6 +202,7 @@ class Context:
|
|
|
200
202
|
system_prompt: str | None = None,
|
|
201
203
|
temperature: float | None = None,
|
|
202
204
|
max_tokens: int | None = None,
|
|
205
|
+
model_preferences: ModelPreferences | str | list[str] | None = None,
|
|
203
206
|
) -> TextContent | ImageContent:
|
|
204
207
|
"""
|
|
205
208
|
Send a sampling request to the client and await the response.
|
|
@@ -231,6 +234,7 @@ class Context:
|
|
|
231
234
|
system_prompt=system_prompt,
|
|
232
235
|
temperature=temperature,
|
|
233
236
|
max_tokens=max_tokens,
|
|
237
|
+
model_preferences=self._parse_model_preferences(model_preferences),
|
|
234
238
|
)
|
|
235
239
|
|
|
236
240
|
return result.content
|
|
@@ -248,3 +252,45 @@ class Context:
|
|
|
248
252
|
)
|
|
249
253
|
|
|
250
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
|
+
)
|
fastmcp/server/http.py
CHANGED
|
@@ -241,6 +241,7 @@ def create_sse_app(
|
|
|
241
241
|
# Add custom routes with lowest precedence
|
|
242
242
|
if routes:
|
|
243
243
|
server_routes.extend(routes)
|
|
244
|
+
server_routes.extend(server._additional_http_routes)
|
|
244
245
|
|
|
245
246
|
# Add middleware
|
|
246
247
|
if middleware:
|
|
@@ -359,6 +360,7 @@ def create_streamable_http_app(
|
|
|
359
360
|
# Add custom routes with lowest precedence
|
|
360
361
|
if routes:
|
|
361
362
|
server_routes.extend(routes)
|
|
363
|
+
server_routes.extend(server._additional_http_routes)
|
|
362
364
|
|
|
363
365
|
# Add middleware
|
|
364
366
|
if middleware:
|