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 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
- uri = AnyUrl(uri) # Ensure AnyUrl
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
 
@@ -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 is particularly useful for testing or when client and server
455
- are in the same process.
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
- self.server = mcp # Can be FastMCP or MCPServer
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
- if len(self.config.mcpServers) == 1:
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 or fn.__doc__,
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 ValueError("Could not convert prompt result to message.")
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 ValueError(f"Error rendering prompt {self.name}.")
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__(self, duplicate_behavior: DuplicateBehavior | None = None):
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
- messages = await prompt.render(arguments)
89
-
90
- return GetPromptResult(description=prompt.description, messages=messages)
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__(self, duplicate_behavior: DuplicateBehavior | None = None):
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
- raise ValueError(f"Error creating resource from template: {e}")
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
- # raise other exceptions as ResourceErrors without revealing internal details
289
+ # Handle other exceptions
269
290
  except Exception as e:
270
291
  logger.error(f"Error reading resource {uri!r}: {e}")
271
- raise ResourceError(f"Error reading resource {uri!r}") from e
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."""
@@ -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
- # Get schema from TypeAdapter - will fail if function isn't properly typed
152
- parameters = TypeAdapter(fn).json_schema()
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 or fn.__doc__ or "",
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: