fastmcp 2.14.1__py3-none-any.whl → 2.14.2__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.
@@ -7,7 +7,7 @@ from typing import Any
7
7
  from mcp import GetPromptResult
8
8
 
9
9
  from fastmcp import settings
10
- from fastmcp.exceptions import NotFoundError, PromptError
10
+ from fastmcp.exceptions import FastMCPError, NotFoundError, PromptError
11
11
  from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
12
12
  from fastmcp.settings import DuplicateBehavior
13
13
  from fastmcp.utilities.logging import get_logger
@@ -107,9 +107,8 @@ class PromptManager:
107
107
  try:
108
108
  messages = await prompt.render(arguments)
109
109
  return GetPromptResult(description=prompt.description, messages=messages)
110
- except PromptError as e:
111
- logger.exception(f"Error rendering prompt {name!r}")
112
- raise e
110
+ except FastMCPError:
111
+ raise
113
112
  except Exception as e:
114
113
  logger.exception(f"Error rendering prompt {name!r}")
115
114
  if self.mask_error_details:
@@ -10,7 +10,7 @@ from typing import Any
10
10
  from pydantic import AnyUrl
11
11
 
12
12
  from fastmcp import settings
13
- from fastmcp.exceptions import NotFoundError, ResourceError
13
+ from fastmcp.exceptions import FastMCPError, NotFoundError, ResourceError
14
14
  from fastmcp.resources.resource import Resource
15
15
  from fastmcp.resources.template import (
16
16
  ResourceTemplate,
@@ -268,10 +268,9 @@ class ResourceManager:
268
268
  uri_str,
269
269
  params=params,
270
270
  )
271
- # Pass through ResourceErrors as-is
272
- except ResourceError as e:
273
- logger.error(f"Error creating resource from template: {e}")
274
- raise e
271
+ # Pass through FastMCPErrors as-is
272
+ except FastMCPError:
273
+ raise
275
274
  # Handle other exceptions
276
275
  except Exception as e:
277
276
  logger.error(f"Error creating resource from template: {e}")
@@ -299,10 +298,9 @@ class ResourceManager:
299
298
  try:
300
299
  return await resource.read()
301
300
 
302
- # raise ResourceErrors as-is
303
- except ResourceError as e:
304
- logger.exception(f"Error reading resource {uri_str!r}")
305
- raise e
301
+ # raise FastMCPErrors as-is
302
+ except FastMCPError:
303
+ raise
306
304
 
307
305
  # Handle other exceptions
308
306
  except Exception as e:
@@ -322,11 +320,8 @@ class ResourceManager:
322
320
  try:
323
321
  resource = await template.create_resource(uri_str, params=params)
324
322
  return await resource.read()
325
- except ResourceError as e:
326
- logger.exception(
327
- f"Error reading resource from template {uri_str!r}"
328
- )
329
- raise e
323
+ except FastMCPError:
324
+ raise
330
325
  except Exception as e:
331
326
  logger.exception(
332
327
  f"Error reading resource from template {uri_str!r}"
@@ -114,6 +114,8 @@ class AuthProvider(TokenVerifierProtocol):
114
114
  base_url = AnyHttpUrl(base_url)
115
115
  self.base_url = base_url
116
116
  self.required_scopes = required_scopes or []
117
+ self._mcp_path: str | None = None
118
+ self._resource_url: AnyHttpUrl | None = None
117
119
 
118
120
  async def verify_token(self, token: str) -> AccessToken | None:
119
121
  """Verify a bearer token and return access info if valid.
@@ -128,6 +130,20 @@ class AuthProvider(TokenVerifierProtocol):
128
130
  """
129
131
  raise NotImplementedError("Subclasses must implement verify_token")
130
132
 
133
+ def set_mcp_path(self, mcp_path: str | None) -> None:
134
+ """Set the MCP endpoint path and compute resource URL.
135
+
136
+ This method is called by get_routes() to configure the expected
137
+ resource URL before route creation. Subclasses can override to
138
+ perform additional initialization that depends on knowing the
139
+ MCP endpoint path.
140
+
141
+ Args:
142
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
143
+ """
144
+ self._mcp_path = mcp_path
145
+ self._resource_url = self._get_resource_url(mcp_path)
146
+
131
147
  def get_routes(
132
148
  self,
133
149
  mcp_path: str | None = None,
@@ -407,6 +423,8 @@ class OAuthProvider(
407
423
  Returns:
408
424
  List of OAuth routes
409
425
  """
426
+ # Configure resource URL before creating routes
427
+ self.set_mcp_path(mcp_path)
410
428
 
411
429
  # Create standard OAuth authorization server routes
412
430
  # Pass base_url as issuer_url to ensure metadata declares endpoints where
@@ -451,11 +469,8 @@ class OAuthProvider(
451
469
  else:
452
470
  oauth_routes.append(route)
453
471
 
454
- # Get the resource URL based on the MCP path
455
- resource_url = self._get_resource_url(mcp_path)
456
-
457
472
  # Add protected resource routes if this server is also acting as a resource server
458
- if resource_url:
473
+ if self._resource_url:
459
474
  supported_scopes = (
460
475
  self.client_registration_options.valid_scopes
461
476
  if self.client_registration_options
@@ -463,7 +478,7 @@ class OAuthProvider(
463
478
  else self.required_scopes
464
479
  )
465
480
  protected_routes = create_protected_resource_routes(
466
- resource_url=resource_url,
481
+ resource_url=self._resource_url,
467
482
  authorization_servers=[cast(AnyHttpUrl, self.issuer_url)],
468
483
  scopes_supported=supported_scopes,
469
484
  )
@@ -34,7 +34,6 @@ from authlib.integrations.httpx_client import AsyncOAuth2Client
34
34
  from cryptography.fernet import Fernet
35
35
  from key_value.aio.adapters.pydantic import PydanticAdapter
36
36
  from key_value.aio.protocols import AsyncKeyValue
37
- from key_value.aio.stores.disk import DiskStore
38
37
  from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
39
38
  from mcp.server.auth.provider import (
40
39
  AccessToken,
@@ -805,14 +804,16 @@ class OAuthProxy(OAuthProvider):
805
804
  salt="fastmcp-jwt-signing-key",
806
805
  )
807
806
 
808
- self._jwt_issuer: JWTIssuer = JWTIssuer(
809
- issuer=str(self.base_url),
810
- audience=f"{str(self.base_url).rstrip('/')}/mcp",
811
- signing_key=jwt_signing_key,
812
- )
807
+ # Store JWT signing key for deferred JWTIssuer creation in set_mcp_path()
808
+ self._jwt_signing_key: bytes = jwt_signing_key
809
+ # JWTIssuer will be created in set_mcp_path() with correct audience
810
+ self._jwt_issuer: JWTIssuer | None = None
813
811
 
814
812
  # If the user does not provide a store, we will provide an encrypted disk store
815
813
  if client_storage is None:
814
+ # Import lazily to avoid sqlite3 dependency when not using OAuthProxy
815
+ from key_value.aio.stores.disk import DiskStore
816
+
816
817
  storage_encryption_key = derive_jwt_key(
817
818
  high_entropy_material=jwt_signing_key.decode(),
818
819
  salt="fastmcp-storage-encryption-key",
@@ -897,6 +898,47 @@ class OAuthProxy(OAuthProvider):
897
898
  self._upstream_authorization_endpoint,
898
899
  )
899
900
 
901
+ # -------------------------------------------------------------------------
902
+ # MCP Path Configuration
903
+ # -------------------------------------------------------------------------
904
+
905
+ def set_mcp_path(self, mcp_path: str | None) -> None:
906
+ """Set the MCP endpoint path and create JWTIssuer with correct audience.
907
+
908
+ This method is called by get_routes() to configure the resource URL
909
+ and create the JWTIssuer. The JWT audience is set to the full resource
910
+ URL (e.g., http://localhost:8000/mcp) to ensure tokens are bound to
911
+ this specific MCP endpoint.
912
+
913
+ Args:
914
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
915
+ """
916
+ super().set_mcp_path(mcp_path)
917
+
918
+ # Create JWT issuer with correct audience based on actual MCP path
919
+ # This ensures tokens are bound to the specific resource URL
920
+ self._jwt_issuer = JWTIssuer(
921
+ issuer=str(self.base_url),
922
+ audience=str(self._resource_url),
923
+ signing_key=self._jwt_signing_key,
924
+ )
925
+
926
+ logger.debug("Configured OAuth proxy for resource URL: %s", self._resource_url)
927
+
928
+ @property
929
+ def jwt_issuer(self) -> JWTIssuer:
930
+ """Get the JWT issuer, ensuring it has been initialized.
931
+
932
+ The JWT issuer is created when set_mcp_path() is called (via get_routes()).
933
+ This property ensures a clear error if used before initialization.
934
+ """
935
+ if self._jwt_issuer is None:
936
+ raise RuntimeError(
937
+ "JWT issuer not initialized. Ensure get_routes() is called "
938
+ "before token operations."
939
+ )
940
+ return self._jwt_issuer
941
+
900
942
  # -------------------------------------------------------------------------
901
943
  # PKCE Helper Methods
902
944
  # -------------------------------------------------------------------------
@@ -998,13 +1040,29 @@ class OAuthProxy(OAuthProvider):
998
1040
  """Start OAuth transaction and route through consent interstitial.
999
1041
 
1000
1042
  Flow:
1001
- 1. Store transaction with client details and PKCE (if forwarding)
1002
- 2. Return local /consent URL; browser visits consent first
1003
- 3. Consent handler redirects to upstream IdP if approved/already approved
1043
+ 1. Validate client's resource matches server's resource URL (security check)
1044
+ 2. Store transaction with client details and PKCE (if forwarding)
1045
+ 3. Return local /consent URL; browser visits consent first
1046
+ 4. Consent handler redirects to upstream IdP if approved/already approved
1004
1047
 
1005
1048
  If consent is disabled (require_authorization_consent=False), skip the consent screen
1006
1049
  and redirect directly to the upstream IdP.
1007
1050
  """
1051
+ # Security check: validate client's requested resource matches this server
1052
+ # This prevents tokens intended for one server from being used on another
1053
+ client_resource = getattr(params, "resource", None)
1054
+ if client_resource and self._resource_url:
1055
+ if str(client_resource) != str(self._resource_url):
1056
+ logger.warning(
1057
+ "Resource mismatch: client requested %s but server is %s",
1058
+ client_resource,
1059
+ self._resource_url,
1060
+ )
1061
+ raise AuthorizeError(
1062
+ error="invalid_target", # type: ignore[arg-type]
1063
+ error_description="Resource does not match this server",
1064
+ )
1065
+
1008
1066
  # Generate transaction ID for this authorization request
1009
1067
  txn_id = secrets.token_urlsafe(32)
1010
1068
 
@@ -1216,7 +1274,7 @@ class OAuthProxy(OAuthProvider):
1216
1274
  # Issue minimal FastMCP access token (just a reference via JTI)
1217
1275
  if client.client_id is None:
1218
1276
  raise TokenError("invalid_client", "Client ID is required")
1219
- fastmcp_access_token = self._jwt_issuer.issue_access_token(
1277
+ fastmcp_access_token = self.jwt_issuer.issue_access_token(
1220
1278
  client_id=client.client_id,
1221
1279
  scopes=authorization_code.scopes,
1222
1280
  jti=access_jti,
@@ -1227,7 +1285,7 @@ class OAuthProxy(OAuthProvider):
1227
1285
  # Use upstream refresh token expiry to align lifetimes
1228
1286
  fastmcp_refresh_token = None
1229
1287
  if refresh_jti and refresh_expires_in:
1230
- fastmcp_refresh_token = self._jwt_issuer.issue_refresh_token(
1288
+ fastmcp_refresh_token = self.jwt_issuer.issue_refresh_token(
1231
1289
  client_id=client.client_id,
1232
1290
  scopes=authorization_code.scopes,
1233
1291
  jti=refresh_jti,
@@ -1352,7 +1410,7 @@ class OAuthProxy(OAuthProvider):
1352
1410
  """
1353
1411
  # Verify FastMCP refresh token
1354
1412
  try:
1355
- refresh_payload = self._jwt_issuer.verify_token(refresh_token.token)
1413
+ refresh_payload = self.jwt_issuer.verify_token(refresh_token.token)
1356
1414
  refresh_jti = refresh_payload["jti"]
1357
1415
  except Exception as e:
1358
1416
  logger.debug("FastMCP refresh token validation failed: %s", e)
@@ -1461,7 +1519,7 @@ class OAuthProxy(OAuthProvider):
1461
1519
  if client.client_id is None:
1462
1520
  raise TokenError("invalid_client", "Client ID is required")
1463
1521
  new_access_jti = secrets.token_urlsafe(32)
1464
- new_fastmcp_access = self._jwt_issuer.issue_access_token(
1522
+ new_fastmcp_access = self.jwt_issuer.issue_access_token(
1465
1523
  client_id=client.client_id,
1466
1524
  scopes=scopes,
1467
1525
  jti=new_access_jti,
@@ -1482,7 +1540,7 @@ class OAuthProxy(OAuthProvider):
1482
1540
  # Issue NEW minimal FastMCP refresh token (rotation for security)
1483
1541
  # Use upstream refresh token expiry to align lifetimes
1484
1542
  new_refresh_jti = secrets.token_urlsafe(32)
1485
- new_fastmcp_refresh = self._jwt_issuer.issue_refresh_token(
1543
+ new_fastmcp_refresh = self.jwt_issuer.issue_refresh_token(
1486
1544
  client_id=client.client_id,
1487
1545
  scopes=scopes,
1488
1546
  jti=new_refresh_jti,
@@ -1558,7 +1616,7 @@ class OAuthProxy(OAuthProvider):
1558
1616
  """
1559
1617
  try:
1560
1618
  # 1. Verify FastMCP JWT signature and claims
1561
- payload = self._jwt_issuer.verify_token(token)
1619
+ payload = self.jwt_issuer.verify_token(token)
1562
1620
  jti = payload["jti"]
1563
1621
 
1564
1622
  # 2. Look up upstream token via JTI mapping
@@ -34,6 +34,7 @@ class SupabaseProviderSettings(BaseSettings):
34
34
 
35
35
  project_url: AnyHttpUrl
36
36
  base_url: AnyHttpUrl
37
+ auth_route: str = "/auth/v1"
37
38
  algorithm: Literal["HS256", "RS256", "ES256"] = "ES256"
38
39
  required_scopes: list[str] | None = None
39
40
 
@@ -59,8 +60,8 @@ class SupabaseProvider(RemoteAuthProvider):
59
60
  - Asymmetric keys (RS256/ES256) are recommended for production
60
61
 
61
62
  2. JWT Verification:
62
- - FastMCP verifies JWTs using the JWKS endpoint at {project_url}/auth/v1/.well-known/jwks.json
63
- - JWTs are issued by {project_url}/auth/v1
63
+ - FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json
64
+ - JWTs are issued by {project_url}{auth_route}
64
65
  - Tokens are cached for up to 10 minutes by Supabase's edge servers
65
66
  - Algorithm must match your Supabase Auth configuration
66
67
 
@@ -93,6 +94,7 @@ class SupabaseProvider(RemoteAuthProvider):
93
94
  *,
94
95
  project_url: AnyHttpUrl | str | NotSetT = NotSet,
95
96
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
97
+ auth_route: str | NotSetT = NotSet,
96
98
  algorithm: Literal["HS256", "RS256", "ES256"] | NotSetT = NotSet,
97
99
  required_scopes: list[str] | NotSetT | None = NotSet,
98
100
  token_verifier: TokenVerifier | None = None,
@@ -102,6 +104,7 @@ class SupabaseProvider(RemoteAuthProvider):
102
104
  Args:
103
105
  project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
104
106
  base_url: Public URL of this FastMCP server
107
+ auth_route: Supabase Auth route. Defaults to "/auth/v1".
105
108
  algorithm: JWT signing algorithm (HS256, RS256, or ES256). Must match your
106
109
  Supabase Auth configuration. Defaults to ES256.
107
110
  required_scopes: Optional list of scopes to require for all requests.
@@ -115,6 +118,7 @@ class SupabaseProvider(RemoteAuthProvider):
115
118
  for k, v in {
116
119
  "project_url": project_url,
117
120
  "base_url": base_url,
121
+ "auth_route": auth_route,
118
122
  "algorithm": algorithm,
119
123
  "required_scopes": required_scopes,
120
124
  }.items()
@@ -124,12 +128,13 @@ class SupabaseProvider(RemoteAuthProvider):
124
128
 
125
129
  self.project_url = str(settings.project_url).rstrip("/")
126
130
  self.base_url = AnyHttpUrl(str(settings.base_url).rstrip("/"))
131
+ self.auth_route = settings.auth_route.strip("/")
127
132
 
128
133
  # Create default JWT verifier if none provided
129
134
  if token_verifier is None:
130
135
  token_verifier = JWTVerifier(
131
- jwks_uri=f"{self.project_url}/auth/v1/.well-known/jwks.json",
132
- issuer=f"{self.project_url}/auth/v1",
136
+ jwks_uri=f"{self.project_url}/{self.auth_route}/.well-known/jwks.json",
137
+ issuer=f"{self.project_url}/{self.auth_route}",
133
138
  algorithm=settings.algorithm,
134
139
  required_scopes=settings.required_scopes,
135
140
  )
@@ -137,7 +142,7 @@ class SupabaseProvider(RemoteAuthProvider):
137
142
  # Initialize RemoteAuthProvider with Supabase as the authorization server
138
143
  super().__init__(
139
144
  token_verifier=token_verifier,
140
- authorization_servers=[AnyHttpUrl(f"{self.project_url}/auth/v1")],
145
+ authorization_servers=[AnyHttpUrl(f"{self.project_url}/{self.auth_route}")],
141
146
  base_url=self.base_url,
142
147
  )
143
148
 
@@ -162,7 +167,7 @@ class SupabaseProvider(RemoteAuthProvider):
162
167
  try:
163
168
  async with httpx.AsyncClient() as client:
164
169
  response = await client.get(
165
- f"{self.project_url}/auth/v1/.well-known/oauth-authorization-server"
170
+ f"{self.project_url}/{self.auth_route}/.well-known/oauth-authorization-server"
166
171
  )
167
172
  response.raise_for_status()
168
173
  metadata = response.json()
@@ -21,6 +21,7 @@ from mcp.server.auth.provider import (
21
21
  from mcp.server.lowlevel.server import request_ctx
22
22
  from starlette.requests import Request
23
23
 
24
+ from fastmcp.exceptions import FastMCPError
24
25
  from fastmcp.server.auth import AccessToken
25
26
  from fastmcp.server.http import _current_http_request
26
27
  from fastmcp.utilities.types import is_class_member_of_type
@@ -188,6 +189,10 @@ async def _resolve_fastmcp_dependencies(
188
189
  resolved[parameter] = await stack.enter_async_context(
189
190
  dependency
190
191
  )
192
+ except FastMCPError:
193
+ # Let FastMCPError subclasses (ToolError, ResourceError, etc.)
194
+ # propagate unchanged so they can be handled appropriately
195
+ raise
191
196
  except Exception as error:
192
197
  fn_name = getattr(fn, "__name__", repr(fn))
193
198
  raise RuntimeError(
@@ -304,15 +304,19 @@ def _dict_to_enum_schema(
304
304
  multi_select: If True, use anyOf pattern; if False, use oneOf pattern
305
305
 
306
306
  Returns:
307
- {"oneOf": [{"const": "low", "title": "Low Priority"}, ...]} for single-select
308
- {"anyOf": [{"const": "low", "title": "Low Priority"}, ...]} for multi-select
307
+ {"type": "string", "oneOf": [...]} for single-select
308
+ {"anyOf": [...]} for multi-select (used as array items)
309
309
  """
310
310
  pattern_key = "anyOf" if multi_select else "oneOf"
311
311
  pattern = []
312
312
  for value, metadata in enum_dict.items():
313
313
  title = metadata.get("title", value)
314
314
  pattern.append({"const": value, "title": title})
315
- return {pattern_key: pattern}
315
+
316
+ result: dict[str, Any] = {pattern_key: pattern}
317
+ if not multi_select:
318
+ result["type"] = "string"
319
+ return result
316
320
 
317
321
 
318
322
  def get_elicitation_schema(response_type: type[T]) -> dict[str, Any]:
@@ -87,7 +87,7 @@ class ErrorHandlingMiddleware(Middleware):
87
87
  return error
88
88
 
89
89
  # Map common exceptions to appropriate MCP error codes
90
- error_type = type(error)
90
+ error_type = type(error.__cause__) if error.__cause__ else type(error)
91
91
 
92
92
  if error_type in (ValueError, TypeError):
93
93
  return McpError(
@@ -64,10 +64,8 @@ class OpenAPITool(Tool):
64
64
  try:
65
65
  # Get base URL from client
66
66
  base_url = (
67
- str(self._client.base_url)
68
- if hasattr(self._client, "base_url") and self._client.base_url
69
- else "http://localhost"
70
- )
67
+ str(self._client.base_url) if hasattr(self._client, "base_url") else ""
68
+ ) or "http://localhost"
71
69
 
72
70
  # Get Headers from client
73
71
  cli_headers = (
fastmcp/server/server.py CHANGED
@@ -105,23 +105,6 @@ if TYPE_CHECKING:
105
105
  logger = get_logger(__name__)
106
106
 
107
107
 
108
- def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
109
- """Create a wrapper function with a custom __name__ for Docket registration.
110
-
111
- Docket uses fn.__name__ as the key for function registration and lookup.
112
- When mounting servers, we need unique names to avoid collisions between
113
- mounted servers that have identically-named functions.
114
- """
115
- import functools
116
-
117
- @functools.wraps(fn)
118
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
119
- return await fn(*args, **kwargs)
120
-
121
- wrapper.__name__ = name
122
- return wrapper
123
-
124
-
125
108
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
126
109
  Transport = Literal["stdio", "http", "sse", "streamable-http"]
127
110
 
@@ -437,7 +420,7 @@ class FastMCP(Generic[LifespanResultT]):
437
420
  isinstance(tool, FunctionTool)
438
421
  and tool.task_config.mode != "forbidden"
439
422
  ):
440
- docket.register(tool.fn)
423
+ docket.register(tool.fn, names=[tool.key])
441
424
 
442
425
  for prompt in self._prompt_manager._prompts.values():
443
426
  if (
@@ -445,21 +428,24 @@ class FastMCP(Generic[LifespanResultT]):
445
428
  and prompt.task_config.mode != "forbidden"
446
429
  ):
447
430
  # task execution requires async fn (validated at creation time)
448
- docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
431
+ docket.register(
432
+ cast(Callable[..., Awaitable[Any]], prompt.fn),
433
+ names=[prompt.key],
434
+ )
449
435
 
450
436
  for resource in self._resource_manager._resources.values():
451
437
  if (
452
438
  isinstance(resource, FunctionResource)
453
439
  and resource.task_config.mode != "forbidden"
454
440
  ):
455
- docket.register(resource.fn)
441
+ docket.register(resource.fn, names=[resource.name])
456
442
 
457
443
  for template in self._resource_manager._templates.values():
458
444
  if (
459
445
  isinstance(template, FunctionResourceTemplate)
460
446
  and template.task_config.mode != "forbidden"
461
447
  ):
462
- docket.register(template.fn)
448
+ docket.register(template.fn, names=[template.name])
463
449
 
464
450
  # Also register functions from mounted servers so tasks can
465
451
  # execute in the parent's Docket context
@@ -489,12 +475,9 @@ class FastMCP(Generic[LifespanResultT]):
489
475
  try:
490
476
  yield
491
477
  finally:
492
- # Cancel worker task on exit with timeout to prevent hanging
493
478
  worker_task.cancel()
494
- with suppress(
495
- asyncio.CancelledError, asyncio.TimeoutError
496
- ):
497
- await asyncio.wait_for(worker_task, timeout=2.0)
479
+ with suppress(asyncio.CancelledError):
480
+ await worker_task
498
481
  finally:
499
482
  _current_worker.reset(worker_token)
500
483
  finally:
@@ -535,8 +518,7 @@ class FastMCP(Generic[LifespanResultT]):
535
518
  fn_name = f"{prefix}_{tool.key}"
536
519
  else:
537
520
  fn_name = tool.key
538
- named_fn = _create_named_fn_wrapper(tool.fn, fn_name)
539
- docket.register(named_fn)
521
+ docket.register(tool.fn, names=[fn_name])
540
522
 
541
523
  # Register prompts with prefixed names
542
524
  for prompt in server._prompt_manager._prompts.values():
@@ -545,10 +527,10 @@ class FastMCP(Generic[LifespanResultT]):
545
527
  and prompt.task_config.mode != "forbidden"
546
528
  ):
547
529
  fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
548
- named_fn = _create_named_fn_wrapper(
549
- cast(Callable[..., Awaitable[Any]], prompt.fn), fn_name
530
+ docket.register(
531
+ cast(Callable[..., Awaitable[Any]], prompt.fn),
532
+ names=[fn_name],
550
533
  )
551
- docket.register(named_fn)
552
534
 
553
535
  # Register resources with prefixed names (use name, not key/URI)
554
536
  for resource in server._resource_manager._resources.values():
@@ -557,8 +539,7 @@ class FastMCP(Generic[LifespanResultT]):
557
539
  and resource.task_config.mode != "forbidden"
558
540
  ):
559
541
  fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
560
- named_fn = _create_named_fn_wrapper(resource.fn, fn_name)
561
- docket.register(named_fn)
542
+ docket.register(resource.fn, names=[fn_name])
562
543
 
563
544
  # Register resource templates with prefixed names (use name, not key/URI)
564
545
  for template in server._resource_manager._templates.values():
@@ -567,8 +548,7 @@ class FastMCP(Generic[LifespanResultT]):
567
548
  and template.task_config.mode != "forbidden"
568
549
  ):
569
550
  fn_name = f"{prefix}_{template.name}" if prefix else template.name
570
- named_fn = _create_named_fn_wrapper(template.fn, fn_name)
571
- docket.register(named_fn)
551
+ docket.register(template.fn, names=[fn_name])
572
552
 
573
553
  # Recursively register from nested mounted servers with accumulated prefix
574
554
  for nested in server._mounted_servers:
@@ -2516,10 +2496,7 @@ class FastMCP(Generic[LifespanResultT]):
2516
2496
  """
2517
2497
  # Display server banner
2518
2498
  if show_banner:
2519
- log_server_banner(
2520
- server=self,
2521
- transport="stdio",
2522
- )
2499
+ log_server_banner(server=self)
2523
2500
 
2524
2501
  with temporary_log_level(log_level):
2525
2502
  async with self._lifespan_manager():
@@ -2582,22 +2559,9 @@ class FastMCP(Generic[LifespanResultT]):
2582
2559
  stateless_http=stateless_http,
2583
2560
  )
2584
2561
 
2585
- # Get the path for the server URL
2586
- server_path = (
2587
- app.state.path.lstrip("/")
2588
- if hasattr(app, "state") and hasattr(app.state, "path")
2589
- else path or ""
2590
- )
2591
-
2592
2562
  # Display server banner
2593
2563
  if show_banner:
2594
- log_server_banner(
2595
- server=self,
2596
- transport=transport,
2597
- host=host,
2598
- port=port,
2599
- path=server_path,
2600
- )
2564
+ log_server_banner(server=self)
2601
2565
  uvicorn_config_from_user = uvicorn_config or {}
2602
2566
 
2603
2567
  config_kwargs: dict[str, Any] = {
fastmcp/tools/tool.py CHANGED
@@ -32,7 +32,7 @@ import fastmcp
32
32
  from fastmcp.server.dependencies import get_context, without_injected_parameters
33
33
  from fastmcp.server.tasks.config import TaskConfig
34
34
  from fastmcp.utilities.components import FastMCPComponent
35
- from fastmcp.utilities.json_schema import compress_schema
35
+ from fastmcp.utilities.json_schema import compress_schema, resolve_root_ref
36
36
  from fastmcp.utilities.logging import get_logger
37
37
  from fastmcp.utilities.types import (
38
38
  Audio,
@@ -559,6 +559,10 @@ class ParsedFunction:
559
559
 
560
560
  output_schema = compress_schema(output_schema, prune_titles=True)
561
561
 
562
+ # Resolve root-level $ref to meet MCP spec requirement for type: object
563
+ # Self-referential Pydantic models generate schemas with $ref at root
564
+ output_schema = resolve_root_ref(output_schema)
565
+
562
566
  except PydanticSchemaGenerationError as e:
563
567
  if "_UnserializableType" not in str(e):
564
568
  logger.debug(f"Unable to generate schema for type {output_type!r}")
@@ -8,7 +8,7 @@ from mcp.types import ToolAnnotations
8
8
  from pydantic import ValidationError
9
9
 
10
10
  from fastmcp import settings
11
- from fastmcp.exceptions import NotFoundError, ToolError
11
+ from fastmcp.exceptions import FastMCPError, NotFoundError, ToolError
12
12
  from fastmcp.settings import DuplicateBehavior
13
13
  from fastmcp.tools.tool import Tool, ToolResult
14
14
  from fastmcp.tools.tool_transform import (
@@ -158,12 +158,10 @@ class ToolManager:
158
158
  tool = await self.get_tool(key)
159
159
  try:
160
160
  return await tool.run(arguments)
161
- except ValidationError as e:
162
- logger.exception(f"Error validating tool {key!r}: {e}")
163
- raise e
164
- except ToolError as e:
165
- logger.exception(f"Error calling tool {key!r}")
166
- raise e
161
+ except FastMCPError:
162
+ raise
163
+ except ValidationError:
164
+ raise
167
165
  except Exception as e:
168
166
  logger.exception(f"Error calling tool {key!r}")
169
167
  if self.mask_error_details:
fastmcp/utilities/cli.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Any, Literal
6
+ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  from pydantic import ValidationError
9
9
  from rich.align import Align
@@ -197,23 +197,8 @@ LOGO_ASCII_4 = (
197
197
  )
198
198
 
199
199
 
200
- def log_server_banner(
201
- server: FastMCP[Any],
202
- transport: Literal["stdio", "http", "sse", "streamable-http"],
203
- *,
204
- host: str | None = None,
205
- port: int | None = None,
206
- path: str | None = None,
207
- ) -> None:
208
- """Creates and logs a formatted banner with server information and logo.
209
-
210
- Args:
211
- transport: The transport protocol being used
212
- server_name: Optional server name to display
213
- host: Host address (for HTTP transports)
214
- port: Port number (for HTTP transports)
215
- path: Server path (for HTTP transports)
216
- """
200
+ def log_server_banner(server: FastMCP[Any]) -> None:
201
+ """Creates and logs a formatted banner with server information and logo."""
217
202
 
218
203
  # Create the logo text
219
204
  # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
@@ -228,39 +213,34 @@ def log_server_banner(
228
213
  info_table.add_column(style="cyan", justify="left") # Label column
229
214
  info_table.add_column(style="dim", justify="left") # Value column
230
215
 
231
- match transport:
232
- case "http" | "streamable-http":
233
- display_transport = "HTTP"
234
- case "sse":
235
- display_transport = "SSE"
236
- case "stdio":
237
- display_transport = "STDIO"
238
-
239
- info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
240
- info_table.add_row("📦", "Transport:", display_transport)
241
-
242
- # Show connection info based on transport
243
- if transport in ("http", "streamable-http", "sse") and host and port:
244
- server_url = f"http://{host}:{port}"
245
- if path:
246
- server_url += f"/{path.lstrip('/')}"
247
- info_table.add_row("🔗", "Server URL:", server_url)
248
-
249
- # Add documentation link
250
- info_table.add_row("", "", "")
251
- info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
252
- info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
216
+ info_table.add_row("🖥", "Server:", Text(server.name, style="dim"))
217
+ info_table.add_row("🚀", "Deploy free:", "https://fastmcp.cloud")
253
218
 
254
219
  # Create panel with logo, title, and information using Group
220
+ docs_url = Text("https://gofastmcp.com", style="dim")
255
221
  panel_content = Group(
222
+ "",
256
223
  Align.center(logo_text),
257
224
  "",
258
- Align.center(title_text),
259
225
  "",
226
+ Align.center(title_text),
227
+ Align.center(docs_url),
260
228
  "",
261
229
  Align.center(info_table),
262
230
  )
263
231
 
232
+ # v3 notice banner (shown below main panel)
233
+ v3_line1 = Text("✨ FastMCP 3.0 is coming!", style="bold")
234
+ v3_line2 = Text(
235
+ "Pin fastmcp<3 in production, then upgrade when you're ready.", style="dim"
236
+ )
237
+ v3_notice = Panel(
238
+ Group(Align.center(v3_line1), Align.center(v3_line2)),
239
+ border_style="blue",
240
+ padding=(0, 2),
241
+ width=80,
242
+ )
243
+
264
244
  panel = Panel(
265
245
  panel_content,
266
246
  border_style="dim",
@@ -270,5 +250,5 @@ def log_server_banner(
270
250
  )
271
251
 
272
252
  console = Console(stderr=True)
273
- # Center the panel itself
274
- console.print(Group("\n", Align.center(panel), "\n"))
253
+ # Center both panels
254
+ console.print(Group("\n", Align.center(panel), Align.center(v3_notice), "\n"))
@@ -1,6 +1,46 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections import defaultdict
4
+ from typing import Any
5
+
6
+
7
+ def resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]:
8
+ """Resolve $ref at root level to meet MCP spec requirements.
9
+
10
+ MCP specification requires outputSchema to have "type": "object" at the root level.
11
+ When Pydantic generates schemas for self-referential models, it uses $ref at the
12
+ root level pointing to $defs. This function resolves such references by inlining
13
+ the referenced definition while preserving $defs for nested references.
14
+
15
+ Args:
16
+ schema: JSON schema dict that may have $ref at root level
17
+
18
+ Returns:
19
+ A new schema dict with root-level $ref resolved, or the original schema
20
+ if no resolution is needed
21
+
22
+ Example:
23
+ >>> schema = {
24
+ ... "$defs": {"Node": {"type": "object", "properties": {...}}},
25
+ ... "$ref": "#/$defs/Node"
26
+ ... }
27
+ >>> resolved = resolve_root_ref(schema)
28
+ >>> # Result: {"type": "object", "properties": {...}, "$defs": {...}}
29
+ """
30
+ # Only resolve if we have $ref at root level with $defs but no explicit type
31
+ if "$ref" in schema and "$defs" in schema and "type" not in schema:
32
+ ref = schema["$ref"]
33
+ # Only handle local $defs references
34
+ if isinstance(ref, str) and ref.startswith("#/$defs/"):
35
+ def_name = ref.split("/")[-1]
36
+ defs = schema["$defs"]
37
+ if def_name in defs:
38
+ # Create a new schema by copying the referenced definition
39
+ resolved = dict(defs[def_name])
40
+ # Preserve $defs for nested references (other fields may still use them)
41
+ resolved["$defs"] = defs
42
+ return resolved
43
+ return schema
4
44
 
5
45
 
6
46
  def _prune_param(schema: dict, param: str) -> dict:
@@ -539,9 +539,9 @@ def extract_output_schema_from_responses(
539
539
  # Replace $ref with the actual schema definition
540
540
  output_schema = _replace_ref_with_defs(schema_definitions[schema_name])
541
541
 
542
- # Convert OpenAPI schema to JSON Schema format
543
- # Only needed for OpenAPI 3.0 - 3.1 uses standard JSON Schema null types
544
- if openapi_version and openapi_version.startswith("3.0"):
542
+ if openapi_version and openapi_version.startswith("3"):
543
+ # Convert OpenAPI 3.x schema to JSON Schema format for proper handling
544
+ # of constructs like oneOf, anyOf, and nullable fields
545
545
  from .json_schema_converter import convert_openapi_schema_to_json_schema
546
546
 
547
547
  output_schema = convert_openapi_schema_to_json_schema(
@@ -570,7 +570,7 @@ def extract_output_schema_from_responses(
570
570
  processed_defs[name] = _replace_ref_with_defs(schema)
571
571
 
572
572
  # Convert OpenAPI schema definitions to JSON Schema format if needed
573
- if openapi_version and openapi_version.startswith("3.0"):
573
+ if openapi_version and openapi_version.startswith("3"):
574
574
  from .json_schema_converter import convert_openapi_schema_to_json_schema
575
575
 
576
576
  for def_name in list(processed_defs.keys()):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.14.1
3
+ Version: 2.14.2
4
4
  Summary: The fast, Pythonic way to build MCP servers and clients.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -23,12 +23,12 @@ Requires-Dist: cyclopts>=4.0.0
23
23
  Requires-Dist: exceptiongroup>=1.2.2
24
24
  Requires-Dist: httpx>=0.28.1
25
25
  Requires-Dist: jsonschema-path>=0.3.4
26
- Requires-Dist: mcp>=1.24.0
26
+ Requires-Dist: mcp<2.0,>=1.24.0
27
27
  Requires-Dist: openapi-pydantic>=0.5.1
28
28
  Requires-Dist: platformdirs>=4.0.0
29
29
  Requires-Dist: py-key-value-aio[disk,keyring,memory]<0.4.0,>=0.3.0
30
30
  Requires-Dist: pydantic[email]>=2.11.7
31
- Requires-Dist: pydocket>=0.15.5
31
+ Requires-Dist: pydocket>=0.16.3
32
32
  Requires-Dist: pyperclip>=1.9.0
33
33
  Requires-Dist: python-dotenv>=1.1.0
34
34
  Requires-Dist: rich>=13.9.4
@@ -75,6 +75,9 @@ Description-Content-Type: text/markdown
75
75
  >
76
76
  > **For production MCP applications, install FastMCP:** `pip install fastmcp`
77
77
 
78
+ > [!Important]
79
+ > FastMCP 3.0 is in development and may include breaking changes. To avoid unexpected issues, pin your dependency to v2: `fastmcp<3`
80
+
78
81
  ---
79
82
 
80
83
  **FastMCP is the standard framework for building MCP applications**, providing the fastest path from idea to production.
@@ -54,26 +54,26 @@ fastmcp/experimental/server/openapi/__init__.py,sha256=QNrM6ZNwJLk78jh7hq1tdZ-Wn
54
54
  fastmcp/experimental/utilities/openapi/__init__.py,sha256=-SIYFQ4CE9MTxKQbksQ4J3lwm409EV3qKkHuTwAEyNk,907
55
55
  fastmcp/prompts/__init__.py,sha256=BQ5ooDJcNhb5maYBcg2mF1VaHAY_A64cEU3UiCQ3Lw8,179
56
56
  fastmcp/prompts/prompt.py,sha256=6Q6xKDIw5MdPC9dTAnM-9FRtA-34dv4qqTrD_5s2w0Y,14483
57
- fastmcp/prompts/prompt_manager.py,sha256=5ZyT0blp5owuaN5pz_TQsyH6zUGFoUiVTGfiEnqBuj8,4262
57
+ fastmcp/prompts/prompt_manager.py,sha256=OJiRAlWLPrPVfyzAzKfP_OHBXKqsU4eotrbUJNABz-A,4205
58
58
  fastmcp/resources/__init__.py,sha256=si8aT_9taxUNN0vkfbifst_SCId56DZmYi4YOb4mtlE,463
59
59
  fastmcp/resources/resource.py,sha256=PNzfTpywc5OIvDtFgAa8SGymzBbpwZCa32Mshh9YcGk,7890
60
- fastmcp/resources/resource_manager.py,sha256=R-dtlhCYHcH1bnGuD0QW5aRUo_12_NeLkn9VLp4xmmU,13308
60
+ fastmcp/resources/resource_manager.py,sha256=yG3EieKY9DqIcYTIFJkSJlRoXeffV6mTOnW3EwpoZfY,13008
61
61
  fastmcp/resources/template.py,sha256=MSAK46bYk74nqJTQ923xb4KETlof9clfg_QaqLrJX_Y,15495
62
62
  fastmcp/resources/types.py,sha256=efFLGD1Xc5Xq3sxlPaZ_8gtJ2UOixueTBV4KQTi4cOU,4936
63
63
  fastmcp/server/__init__.py,sha256=qxNmIJcqsrpxpUvCv0mhdEAaUn1UZd1xLd8XRoWUlfY,119
64
64
  fastmcp/server/context.py,sha256=nd0bME6I7aB9RlKiESNBay97P8yDHJJ-IcjVCbkc7bc,42256
65
- fastmcp/server/dependencies.py,sha256=7As8f1yRIQI0dSDpLVfSCPMXxaPuhUrDvNpxwjSG6W0,20643
66
- fastmcp/server/elicitation.py,sha256=jmrLb_yzdmM9hVRxOYlC4aWnlCBCayuzVOs7--sByxU,17862
65
+ fastmcp/server/dependencies.py,sha256=gRc60PhEvna9rlqMW-ZlYNszPlUeEeOWT5winYGNH2A,20928
66
+ fastmcp/server/elicitation.py,sha256=CmHi_SERmhEcNjwnM90_HGihUKlCM3RPGHI0uns2t7M,17912
67
67
  fastmcp/server/event_store.py,sha256=ZiBbrUQHw9--G8lzK1qLZmUAF2le2XchFen4pGbFKsE,6170
68
68
  fastmcp/server/http.py,sha256=_HjMSYWH8mfKugDODU4iV0AhKDU2VRc40tS56L6i-_s,12737
69
69
  fastmcp/server/low_level.py,sha256=o3jDf5SuZBQeurhLWRzaSVCnvrmaKMH_w-TbHk6BuZ4,7963
70
70
  fastmcp/server/proxy.py,sha256=bsgVkcdlRtVK3bB4EeVKrq4PLjIoUvWN_hgzr1hq8yE,26837
71
- fastmcp/server/server.py,sha256=e9a-0oiZGSavynLLc3mctw1R4avU5PpIqyOrTOKK7YM,122647
71
+ fastmcp/server/server.py,sha256=3RmmJT2vM-ycXces_L6ORSQqlQvzMVITu37dCgqp-Ao,121385
72
72
  fastmcp/server/auth/__init__.py,sha256=MTZvDKEUMqjs9-raRN0h8Zjx8pWFXs_iSRbB1UqBUqU,527
73
- fastmcp/server/auth/auth.py,sha256=6FIjQLhQ-ps58o05nrOi5pyD1OmM0fANrV-M79Jb7do,19775
73
+ fastmcp/server/auth/auth.py,sha256=Bvm98USOP0A0yTckKCN7yHJHS4JgCG804W5cQx6GgO4,20430
74
74
  fastmcp/server/auth/jwt_issuer.py,sha256=lJYvrpC1ygI4jkoJlL_nTH6m7FKdTw2lbEycKo4eHLY,7197
75
75
  fastmcp/server/auth/middleware.py,sha256=xwj3fUCLSlJK6n1Ehp-FN1qnjKqEz8b7LGAGMTqQ8Hk,3284
76
- fastmcp/server/auth/oauth_proxy.py,sha256=Kk09KEK5YwZ2TnVbevqo0WuflBNz5kwWbOXq2St6itE,90694
76
+ fastmcp/server/auth/oauth_proxy.py,sha256=c5BAyT07t3vZtfD9wCoN4f8hnSD1F5e0kCMqlUuO_00,93364
77
77
  fastmcp/server/auth/oidc_proxy.py,sha256=gU_RgBbVMj-9vn0TSRTmT1YaT19VFmJLpARcIXn208k,17969
78
78
  fastmcp/server/auth/redirect_validation.py,sha256=Jlhela9xpTbw4aWnQ04A5Z-TW0HYOC3f9BMsq3NXx1Q,2000
79
79
  fastmcp/server/auth/handlers/authorize.py,sha256=1zrmXqRUhjiWSHgUhfj0CcCkj3uSlGkTnxHzaic0xYs,11617
@@ -91,11 +91,11 @@ fastmcp/server/auth/providers/introspection.py,sha256=v2hlcuxxwug5myCr4KcTZlawwa
91
91
  fastmcp/server/auth/providers/jwt.py,sha256=c-2Wji-CvuYt3U3unxjJR-5-EABRDks_755EpxKBDH8,20798
92
92
  fastmcp/server/auth/providers/oci.py,sha256=QxpsStKEyl_W4dcJOky4m6wdpGnCSnt7WQ8DWjGPmSU,9894
93
93
  fastmcp/server/auth/providers/scalekit.py,sha256=30J2HImUAkyknMgH7lUGytcDOy4d01ClxTrBCO4E3GQ,9064
94
- fastmcp/server/auth/providers/supabase.py,sha256=9aK9fZ2OtccOF-ittMJnwj6sEzUNUTIrRPWAPLMwCac,7321
94
+ fastmcp/server/auth/providers/supabase.py,sha256=T3Qq1mkkzZ9T9ah3uK7qRuMMLWeD_3eRLJRnpiqgTiY,7618
95
95
  fastmcp/server/auth/providers/workos.py,sha256=_KWsgKPV4OJ6a37FaVgq2LIzM3Nx26G5QQhgS8x2MO4,17244
96
96
  fastmcp/server/middleware/__init__.py,sha256=LXT2IcZI4gbAtR4TnA7v_1lOWBR6eaHiE3Cp32Pv0bc,155
97
97
  fastmcp/server/middleware/caching.py,sha256=xYUXkFeuoLaIJ_TB2570qEBS1TtneJClJOpJGNsNbu8,18414
98
- fastmcp/server/middleware/error_handling.py,sha256=eSMKrmIxDcnhzLGyOL49hup5k5e0iwvH_n2XVxJ69W8,7726
98
+ fastmcp/server/middleware/error_handling.py,sha256=TqERAA3qMvqb0Q0N_rwD5iOhoefOW2WT9IGSUZIWFik,7772
99
99
  fastmcp/server/middleware/logging.py,sha256=Reta-f4z8suYkJn4rPyJWYrNBeU25w8Y40U0uaV9ygo,9427
100
100
  fastmcp/server/middleware/middleware.py,sha256=-L4QuyyjIF1QIcydWzamrmpIE2w7d2f35-QyoXMZnZM,6643
101
101
  fastmcp/server/middleware/rate_limiting.py,sha256=MwhMOhgsIhZjYwEQB8H8961hohV5564JlTwwYy_9ctU,7915
@@ -103,7 +103,7 @@ fastmcp/server/middleware/timing.py,sha256=lL_xc-ErLD5lplfvd5-HIyWEbZhgNBYkcQ74K
103
103
  fastmcp/server/middleware/tool_injection.py,sha256=zElqBN-yjZvcTADp57e0dn86kpxT9xsFqvYztiXuA08,3595
104
104
  fastmcp/server/openapi/README.md,sha256=1Mc1Ur15OxMn-wAPEa1rZIiNNSMdv9sboQ3YpvNpUXM,9886
105
105
  fastmcp/server/openapi/__init__.py,sha256=cZPebMY9xwjW8nUgTN5MvawnZEFx9E0Oe_TFqSrevp0,728
106
- fastmcp/server/openapi/components.py,sha256=lHT3AJUKDt68-x7RpErP2ePLJp12HKKUn1VWq5TU6Ss,13346
106
+ fastmcp/server/openapi/components.py,sha256=VdCwdyFh46Y8YIhz5qq1yVXhrQnIWu_KzWi9Ea2HOLc,13294
107
107
  fastmcp/server/openapi/routing.py,sha256=_WWci6GNqtfF-5yO-uHwXXc9nNFNV-YlbIWHa7-lCk4,4018
108
108
  fastmcp/server/openapi/server.py,sha256=aQ_VwvHxdsC-O-7k_uKmPDkOlcgtOW-gk-RtlLtEtuw,16069
109
109
  fastmcp/server/sampling/__init__.py,sha256=u9jDHSE_yz6kTzbFqIOXqnM0PfIAiP-peAjHJBNqDd0,249
@@ -118,17 +118,17 @@ fastmcp/server/tasks/keys.py,sha256=w9diycj0N6ViVqe6stxUS9vg2H94bl_614Bu5kNRM-k,
118
118
  fastmcp/server/tasks/protocol.py,sha256=g97D4k1U8ua_UBTyoqFXcPp5rf6KvuiY5d6mx5KMIPY,12222
119
119
  fastmcp/server/tasks/subscriptions.py,sha256=iehPO2zx80aRIqKHCFj9kuR5NVMqYSkIepMXBifQFWw,6692
120
120
  fastmcp/tools/__init__.py,sha256=XGcaMkBgwr-AHzbNjyjdb3ATgp5TQ0wzSq0nsrBD__E,201
121
- fastmcp/tools/tool.py,sha256=nED1tGlB-8v4DLBweIhHzAjw92whty-LcB1yCmEHv3I,23137
122
- fastmcp/tools/tool_manager.py,sha256=pCQGvKimXYEigcUqRHBd6_mbfJwD2KN3i0SmFj9Fj_c,5913
121
+ fastmcp/tools/tool.py,sha256=_l0HEnuTyYxm_xNWYxO2seRnzb6NunvjnEsWQIeKBDY,23394
122
+ fastmcp/tools/tool_manager.py,sha256=_SSHYgKygZaJ86B2pncmBm2Kbj0NLIDrpphsc9qgB3M,5788
123
123
  fastmcp/tools/tool_transform.py,sha256=m1XDYuu_BDPxpH3yRNdT3jCca9KmVSO-Jd00BK4F5rw,38099
124
124
  fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
125
125
  fastmcp/utilities/auth.py,sha256=ZVHkNb4YBpLE1EmmFyhvFB2qfWDZdEYNH9TRI9jylOE,1140
126
- fastmcp/utilities/cli.py,sha256=46gyOddE8kWhUV2lHFM7kA2v0YNyzcajvIX3Db8gJXk,12174
126
+ fastmcp/utilities/cli.py,sha256=qjZtF9LvReKDOaLDIzcWWErMpWUBA8-Uvml8H4upuqs,11515
127
127
  fastmcp/utilities/components.py,sha256=fF4M9cdqbZTlDAZ0hltcTTg_8IU2jNSzOyH4oqH49ig,6087
128
128
  fastmcp/utilities/exceptions.py,sha256=7Z9j5IzM5rT27BC1Mcn8tkS-bjqCYqMKwb2MMTaxJYU,1350
129
129
  fastmcp/utilities/http.py,sha256=1ns1ymBS-WSxbZjGP6JYjSO52Wa_ls4j4WbnXiupoa4,245
130
130
  fastmcp/utilities/inspect.py,sha256=3wYUuQH1xCCCdzZwALHNqaRABH6iqpA43dIXEhqVb5Q,18030
131
- fastmcp/utilities/json_schema.py,sha256=-XjtAVzCaaJ_S-HoWo7Aabvlu8ubBqyoOinm9E85F4o,8888
131
+ fastmcp/utilities/json_schema.py,sha256=H8RNucfulnXqYjCzRrlaWCBfToHmJGc7M32VJu5q7Eo,10587
132
132
  fastmcp/utilities/json_schema_type.py,sha256=5cf1ZeHzqirrGx62kznqmgAWk0uCc29REVKcDRBeJX0,22348
133
133
  fastmcp/utilities/logging.py,sha256=61wVk5yQ62km3K8kZtkKtT_3EN26VL85GYW0aMtnwKA,7175
134
134
  fastmcp/utilities/mcp_config.py,sha256=lVllZtAXZ3Zy78D40aXN-S5fs-ms0lgryL1tY2WzwCY,1783
@@ -152,9 +152,9 @@ fastmcp/utilities/openapi/formatters.py,sha256=AWyETOfnBmTUcD1T2ajfkbsVyyMnN4tZ-
152
152
  fastmcp/utilities/openapi/json_schema_converter.py,sha256=PxaYpgHBsdDTT0XSP6s4RZBMeDpAO_-dRXlBF2iYD9s,13089
153
153
  fastmcp/utilities/openapi/models.py,sha256=-kfndwZSe92tVtKAgOuFn5rk1tN7oydCZKtLOEMEalA,2805
154
154
  fastmcp/utilities/openapi/parser.py,sha256=qsa68Ro1c8ov77kdEP20IwZqD74E4IGKjtfeIkn3HdE,34338
155
- fastmcp/utilities/openapi/schemas.py,sha256=84nPtnOlfjNoFGDoVoWLs0dh_7Ps92p3AuHgpVA5a-s,23349
156
- fastmcp-2.14.1.dist-info/METADATA,sha256=aNyfWR6oKRLz0VSxRKuof27SbNeQvjvve0X3SxXwOa4,20617
157
- fastmcp-2.14.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
158
- fastmcp-2.14.1.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
159
- fastmcp-2.14.1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
160
- fastmcp-2.14.1.dist-info/RECORD,,
155
+ fastmcp/utilities/openapi/schemas.py,sha256=UXHHjkJyDp1WwJ8kowYt79wnwdbDwAbUFfqwcIY6mIM,23359
156
+ fastmcp-2.14.2.dist-info/METADATA,sha256=pKt4x3lDwdG_CrWNz0oKxcuCYizqMuhFpUjK1vQsdCQ,20771
157
+ fastmcp-2.14.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
158
+ fastmcp-2.14.2.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
159
+ fastmcp-2.14.2.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
160
+ fastmcp-2.14.2.dist-info/RECORD,,