fastmcp 2.14.2__py3-none-any.whl → 2.14.4__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/cli/cli.py CHANGED
@@ -28,6 +28,7 @@ from fastmcp.utilities.inspect import (
28
28
  )
29
29
  from fastmcp.utilities.logging import get_logger
30
30
  from fastmcp.utilities.mcp_server_config import MCPServerConfig
31
+ from fastmcp.utilities.version_check import check_for_newer_version
31
32
 
32
33
  logger = get_logger("cli")
33
34
  console = Console()
@@ -122,6 +123,14 @@ def version(
122
123
  else:
123
124
  console.print(g)
124
125
 
126
+ # Check for updates (not included in --copy output)
127
+ if newer_version := check_for_newer_version():
128
+ console.print()
129
+ console.print(
130
+ f"[bold]🎉 FastMCP update available:[/bold] [green]{newer_version}[/green]"
131
+ )
132
+ console.print("[dim]Run: pip install --upgrade fastmcp[/dim]")
133
+
125
134
 
126
135
  @app.command
127
136
  async def dev(
@@ -105,10 +105,13 @@ class TokenStorageAdapter(TokenStorage):
105
105
 
106
106
  @override
107
107
  async def set_tokens(self, tokens: OAuthToken) -> None:
108
+ # Don't set TTL based on access token expiry - the refresh token may be
109
+ # valid much longer. Use 1 year as a reasonable upper bound; the OAuth
110
+ # provider handles actual token expiry/refresh logic.
108
111
  await self._storage_oauth_token.put(
109
112
  key=self._get_token_cache_key(),
110
113
  value=tokens,
111
- ttl=tokens.expires_in,
114
+ ttl=60 * 60 * 24 * 365, # 1 year
112
115
  )
113
116
 
114
117
  @override
@@ -25,7 +25,7 @@ from mcp.client.sse import sse_client
25
25
  from mcp.client.stdio import stdio_client
26
26
  from mcp.client.streamable_http import streamable_http_client
27
27
  from mcp.server.fastmcp import FastMCP as FastMCP1Server
28
- from mcp.shared._httpx_utils import McpHttpClientFactory
28
+ from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
29
29
  from mcp.shared.memory import create_client_server_memory_streams
30
30
  from pydantic import AnyUrl
31
31
  from typing_extensions import TypedDict, Unpack
@@ -36,7 +36,6 @@ from fastmcp.client.auth.oauth import OAuth
36
36
  from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
37
37
  from fastmcp.server.dependencies import get_http_headers
38
38
  from fastmcp.server.server import FastMCP
39
- from fastmcp.server.tasks.capabilities import get_task_capabilities
40
39
  from fastmcp.utilities.logging import get_logger
41
40
  from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
42
41
 
@@ -284,25 +283,31 @@ class StreamableHttpTransport(ClientTransport):
284
283
  # need to be forwarded to the remote server.
285
284
  headers = get_http_headers() | self.headers
286
285
 
287
- # Build httpx client configuration
288
- httpx_client_kwargs: dict[str, Any] = {
289
- "headers": headers,
290
- "auth": self.auth,
291
- "follow_redirects": True,
292
- }
293
-
294
- # Configure timeout if provided (convert timedelta to seconds for httpx)
286
+ # Configure timeout if provided, preserving MCP's 30s connect default
287
+ timeout: httpx.Timeout | None = None
295
288
  if session_kwargs.get("read_timeout_seconds") is not None:
296
289
  read_timeout_seconds = cast(
297
290
  datetime.timedelta, session_kwargs.get("read_timeout_seconds")
298
291
  )
299
- httpx_client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
292
+ timeout = httpx.Timeout(30.0, read=read_timeout_seconds.total_seconds())
300
293
 
301
- # Create httpx client from factory or use default
294
+ # Create httpx client from factory or use default with MCP-appropriate timeouts
295
+ # create_mcp_http_client uses 30s connect/5min read timeout by default,
296
+ # and always enables follow_redirects
302
297
  if self.httpx_client_factory is not None:
303
- http_client = self.httpx_client_factory(**httpx_client_kwargs)
298
+ # Factory clients get the full kwargs for backwards compatibility
299
+ http_client = self.httpx_client_factory(
300
+ headers=headers,
301
+ auth=self.auth,
302
+ follow_redirects=True,
303
+ **({"timeout": timeout} if timeout else {}),
304
+ )
304
305
  else:
305
- http_client = httpx.AsyncClient(**httpx_client_kwargs)
306
+ http_client = create_mcp_http_client(
307
+ headers=headers,
308
+ timeout=timeout,
309
+ auth=self.auth,
310
+ )
306
311
 
307
312
  # Ensure httpx client is closed after use
308
313
  async with (
@@ -894,16 +899,11 @@ class FastMCPTransport(ClientTransport):
894
899
  anyio.create_task_group() as tg,
895
900
  _enter_server_lifespan(server=self.server),
896
901
  ):
897
- # Build experimental capabilities
898
- experimental_capabilities = get_task_capabilities()
899
-
900
902
  tg.start_soon(
901
903
  lambda: self.server._mcp_server.run(
902
904
  server_read,
903
905
  server_write,
904
- self.server._mcp_server.create_initialization_options(
905
- experimental_capabilities=experimental_capabilities
906
- ),
906
+ self.server._mcp_server.create_initialization_options(),
907
907
  raise_exceptions=self.raise_exceptions,
908
908
  )
909
909
  )
@@ -1221,12 +1221,24 @@ class OAuthProxy(OAuthProvider):
1221
1221
  # - 1 year if no refresh token (likely API-key-style token like GitHub OAuth Apps)
1222
1222
  if "expires_in" in idp_tokens:
1223
1223
  expires_in = int(idp_tokens["expires_in"])
1224
+ logger.debug(
1225
+ "Access token TTL: %d seconds (from IdP expires_in)", expires_in
1226
+ )
1224
1227
  elif self._fallback_access_token_expiry_seconds is not None:
1225
1228
  expires_in = self._fallback_access_token_expiry_seconds
1229
+ logger.debug(
1230
+ "Access token TTL: %d seconds (using configured fallback)", expires_in
1231
+ )
1226
1232
  elif idp_tokens.get("refresh_token"):
1227
1233
  expires_in = DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS
1234
+ logger.debug(
1235
+ "Access token TTL: %d seconds (default, has refresh token)", expires_in
1236
+ )
1228
1237
  else:
1229
1238
  expires_in = DEFAULT_ACCESS_TOKEN_EXPIRY_NO_REFRESH_SECONDS
1239
+ logger.debug(
1240
+ "Access token TTL: %d seconds (default, no refresh token)", expires_in
1241
+ )
1230
1242
 
1231
1243
  # Calculate refresh token expiry if provided by upstream
1232
1244
  # Some providers include refresh_expires_in, some don't
@@ -1266,8 +1278,9 @@ class OAuthProxy(OAuthProvider):
1266
1278
  await self._upstream_token_store.put(
1267
1279
  key=upstream_token_id,
1268
1280
  value=upstream_token_set,
1269
- ttl=refresh_expires_in
1270
- or expires_in, # Auto-expire when refresh token, or access token expires
1281
+ ttl=max(
1282
+ refresh_expires_in or 0, expires_in, 1
1283
+ ), # Keep until longest-lived token expires (min 1s for safety)
1271
1284
  )
1272
1285
  logger.debug("Stored encrypted upstream tokens (jti=%s)", access_jti[:8])
1273
1286
 
@@ -1467,10 +1480,21 @@ class OAuthProxy(OAuthProvider):
1467
1480
  # (user override still applies if set)
1468
1481
  if "expires_in" in token_response:
1469
1482
  new_expires_in = int(token_response["expires_in"])
1483
+ logger.debug(
1484
+ "Refreshed access token TTL: %d seconds (from IdP expires_in)",
1485
+ new_expires_in,
1486
+ )
1470
1487
  elif self._fallback_access_token_expiry_seconds is not None:
1471
1488
  new_expires_in = self._fallback_access_token_expiry_seconds
1489
+ logger.debug(
1490
+ "Refreshed access token TTL: %d seconds (using configured fallback)",
1491
+ new_expires_in,
1492
+ )
1472
1493
  else:
1473
1494
  new_expires_in = DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS
1495
+ logger.debug(
1496
+ "Refreshed access token TTL: %d seconds (default)", new_expires_in
1497
+ )
1474
1498
  upstream_token_set.access_token = token_response["access_token"]
1475
1499
  upstream_token_set.expires_at = time.time() + new_expires_in
1476
1500
 
@@ -1504,15 +1528,18 @@ class OAuthProxy(OAuthProvider):
1504
1528
  )
1505
1529
 
1506
1530
  upstream_token_set.raw_token_data = token_response
1531
+ # Calculate refresh TTL for storage
1532
+ refresh_ttl = new_refresh_expires_in or (
1533
+ int(upstream_token_set.refresh_token_expires_at - time.time())
1534
+ if upstream_token_set.refresh_token_expires_at
1535
+ else 60 * 60 * 24 * 30 # Default to 30 days if unknown
1536
+ )
1507
1537
  await self._upstream_token_store.put(
1508
1538
  key=upstream_token_set.upstream_token_id,
1509
1539
  value=upstream_token_set,
1510
- ttl=new_refresh_expires_in
1511
- or (
1512
- int(upstream_token_set.refresh_token_expires_at - time.time())
1513
- if upstream_token_set.refresh_token_expires_at
1514
- else 60 * 60 * 24 * 30 # Default to 30 days if unknown
1515
- ), # Auto-expire when refresh token expires
1540
+ ttl=max(
1541
+ refresh_ttl, new_expires_in, 1
1542
+ ), # Keep until longest-lived token expires (min 1s for safety)
1516
1543
  )
1517
1544
 
1518
1545
  # Issue new minimal FastMCP access token (just a reference via JTI)
@@ -1549,7 +1576,7 @@ class OAuthProxy(OAuthProvider):
1549
1576
  )
1550
1577
 
1551
1578
  # Store new refresh token JTI mapping with aligned expiry
1552
- refresh_ttl = new_refresh_expires_in or 60 * 60 * 24 * 30
1579
+ # (reuse refresh_ttl calculated above for upstream token store)
1553
1580
  await self._jti_mapping_store.put(
1554
1581
  key=new_refresh_jti,
1555
1582
  value=JTIMapping(
@@ -1622,7 +1649,10 @@ class OAuthProxy(OAuthProvider):
1622
1649
  # 2. Look up upstream token via JTI mapping
1623
1650
  jti_mapping = await self._jti_mapping_store.get(key=jti)
1624
1651
  if not jti_mapping:
1625
- logger.debug("JTI mapping not found: %s", jti)
1652
+ logger.info(
1653
+ "JTI mapping not found (token may have expired): jti=%s...",
1654
+ jti[:16],
1655
+ )
1626
1656
  return None
1627
1657
 
1628
1658
  upstream_token_set = await self._upstream_token_store.get(
@@ -1865,6 +1895,11 @@ class OAuthProxy(OAuthProvider):
1865
1895
  logger.debug(
1866
1896
  f"Successfully exchanged IdP code for tokens (transaction: {txn_id}, PKCE: {bool(proxy_code_verifier)})"
1867
1897
  )
1898
+ logger.debug(
1899
+ "IdP token response: expires_in=%s, has_refresh_token=%s",
1900
+ idp_tokens.get("expires_in"),
1901
+ "refresh_token" in idp_tokens,
1902
+ )
1868
1903
 
1869
1904
  except Exception as e:
1870
1905
  logger.error("IdP token exchange failed: %s", e)
fastmcp/server/context.py CHANGED
@@ -185,10 +185,24 @@ class Context:
185
185
  self._tokens.append(token)
186
186
 
187
187
  # Set current server for dependency injection (use weakref to avoid reference cycles)
188
- from fastmcp.server.dependencies import _current_server
188
+ from fastmcp.server.dependencies import (
189
+ _current_docket,
190
+ _current_server,
191
+ _current_worker,
192
+ )
189
193
 
190
194
  self._server_token = _current_server.set(weakref.ref(self.fastmcp))
191
195
 
196
+ # Set docket/worker from server instance for this request's context.
197
+ # This ensures ContextVars work even in environments (like Lambda) where
198
+ # lifespan ContextVars don't propagate to request handlers.
199
+ server = self.fastmcp
200
+ if server._docket is not None:
201
+ self._docket_token = _current_docket.set(server._docket)
202
+
203
+ if server._worker is not None:
204
+ self._worker_token = _current_worker.set(server._worker)
205
+
192
206
  return self
193
207
 
194
208
  async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
@@ -196,10 +210,20 @@ class Context:
196
210
  # Flush any remaining notifications before exiting
197
211
  await self._flush_notifications()
198
212
 
199
- # Reset server token
200
- if hasattr(self, "_server_token"):
201
- from fastmcp.server.dependencies import _current_server
213
+ # Reset server/docket/worker tokens
214
+ from fastmcp.server.dependencies import (
215
+ _current_docket,
216
+ _current_server,
217
+ _current_worker,
218
+ )
202
219
 
220
+ if hasattr(self, "_worker_token"):
221
+ _current_worker.reset(self._worker_token)
222
+ delattr(self, "_worker_token")
223
+ if hasattr(self, "_docket_token"):
224
+ _current_docket.reset(self._docket_token)
225
+ delattr(self, "_docket_token")
226
+ if hasattr(self, "_server_token"):
203
227
  _current_server.reset(self._server_token)
204
228
  delattr(self, "_server_token")
205
229
 
fastmcp/server/http.py CHANGED
@@ -21,7 +21,6 @@ from starlette.types import Lifespan, Receive, Scope, Send
21
21
 
22
22
  from fastmcp.server.auth import AuthProvider
23
23
  from fastmcp.server.auth.middleware import RequireAuthMiddleware
24
- from fastmcp.server.tasks.capabilities import get_task_capabilities
25
24
  from fastmcp.utilities.logging import get_logger
26
25
 
27
26
  if TYPE_CHECKING:
@@ -160,15 +159,10 @@ def create_sse_app(
160
159
  # Create handler for SSE connections
161
160
  async def handle_sse(scope: Scope, receive: Receive, send: Send) -> Response:
162
161
  async with sse.connect_sse(scope, receive, send) as streams:
163
- # Build experimental capabilities
164
- experimental_capabilities = get_task_capabilities()
165
-
166
162
  await server._mcp_server.run(
167
163
  streams[0],
168
164
  streams[1],
169
- server._mcp_server.create_initialization_options(
170
- experimental_capabilities=experimental_capabilities
171
- ),
165
+ server._mcp_server.create_initialization_options(),
172
166
  )
173
167
  return Response()
174
168
 
@@ -163,6 +163,31 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
163
163
  **kwargs,
164
164
  )
165
165
 
166
+ def get_capabilities(
167
+ self,
168
+ notification_options: NotificationOptions,
169
+ experimental_capabilities: dict[str, dict[str, Any]],
170
+ ) -> mcp.types.ServerCapabilities:
171
+ """Override to set capabilities.tasks as a first-class field per SEP-1686.
172
+
173
+ This ensures task capabilities appear in capabilities.tasks instead of
174
+ capabilities.experimental.tasks, which is required by the MCP spec and
175
+ enables proper task detection by clients like VS Code Copilot 1.107+.
176
+ """
177
+ from fastmcp.server.tasks.capabilities import get_task_capabilities
178
+
179
+ # Get base capabilities from SDK (pass empty dict for experimental)
180
+ # since we'll set tasks as a first-class field instead
181
+ capabilities = super().get_capabilities(
182
+ notification_options,
183
+ experimental_capabilities or {},
184
+ )
185
+
186
+ # Set tasks as a first-class field (not experimental) per SEP-1686
187
+ capabilities.tasks = get_task_capabilities()
188
+
189
+ return capabilities
190
+
166
191
  async def run(
167
192
  self,
168
193
  read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
fastmcp/server/server.py CHANGED
@@ -75,7 +75,6 @@ from fastmcp.server.http import (
75
75
  )
76
76
  from fastmcp.server.low_level import LowLevelServer
77
77
  from fastmcp.server.middleware import Middleware, MiddlewareContext
78
- from fastmcp.server.tasks.capabilities import get_task_capabilities
79
78
  from fastmcp.server.tasks.config import TaskConfig
80
79
  from fastmcp.server.tasks.handlers import (
81
80
  handle_prompt_as_task,
@@ -197,8 +196,9 @@ class FastMCP(Generic[LifespanResultT]):
197
196
  # Resolve server default for background task support
198
197
  self._support_tasks_by_default: bool = tasks if tasks is not None else False
199
198
 
200
- # Docket instance (set during lifespan for cross-task access)
199
+ # Docket and Worker instances (set during lifespan for cross-task access)
201
200
  self._docket = None
201
+ self._worker = None
202
202
 
203
203
  self._additional_http_routes: list[BaseRoute] = []
204
204
  self._mounted_servers: list[MountedServer] = []
@@ -468,6 +468,8 @@ class FastMCP(Generic[LifespanResultT]):
468
468
 
469
469
  # Create and start Worker
470
470
  async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
471
+ # Store on server instance for cross-context access
472
+ self._worker = worker
471
473
  # Set Worker in ContextVar so CurrentWorker can access it
472
474
  worker_token = _current_worker.set(worker)
473
475
  try:
@@ -480,13 +482,11 @@ class FastMCP(Generic[LifespanResultT]):
480
482
  await worker_task
481
483
  finally:
482
484
  _current_worker.reset(worker_token)
485
+ self._worker = None
483
486
  finally:
484
- # Reset ContextVar
485
487
  _current_docket.reset(docket_token)
486
- # Clear instance attribute
487
488
  self._docket = None
488
489
  finally:
489
- # Reset server ContextVar
490
490
  _current_server.reset(server_token)
491
491
 
492
492
  async def _register_mounted_server_functions(
@@ -564,6 +564,8 @@ class FastMCP(Generic[LifespanResultT]):
564
564
  @asynccontextmanager
565
565
  async def _lifespan_manager(self) -> AsyncIterator[None]:
566
566
  if self._lifespan_result_set:
567
+ # Lifespan already ran - ContextVars will be set by Context.__aenter__
568
+ # at request time, so we just yield here.
567
569
  yield
568
570
  return
569
571
 
@@ -2505,9 +2507,6 @@ class FastMCP(Generic[LifespanResultT]):
2505
2507
  f"Starting MCP server {self.name!r} with transport 'stdio'"
2506
2508
  )
2507
2509
 
2508
- # Build experimental capabilities
2509
- experimental_capabilities = get_task_capabilities()
2510
-
2511
2510
  await self._mcp_server.run(
2512
2511
  read_stream,
2513
2512
  write_stream,
@@ -2515,7 +2514,6 @@ class FastMCP(Generic[LifespanResultT]):
2515
2514
  notification_options=NotificationOptions(
2516
2515
  tools_changed=True
2517
2516
  ),
2518
- experimental_capabilities=experimental_capabilities,
2519
2517
  ),
2520
2518
  )
2521
2519
 
@@ -1,22 +1,30 @@
1
1
  """SEP-1686 task capabilities declaration."""
2
2
 
3
- from typing import Any
3
+ from mcp.types import (
4
+ ServerTasksCapability,
5
+ ServerTasksRequestsCapability,
6
+ TasksCallCapability,
7
+ TasksCancelCapability,
8
+ TasksListCapability,
9
+ TasksToolsCapability,
10
+ )
4
11
 
5
12
 
6
- def get_task_capabilities() -> dict[str, Any]:
7
- """Return the SEP-1686 task capabilities structure.
13
+ def get_task_capabilities() -> ServerTasksCapability:
14
+ """Return the SEP-1686 task capabilities.
8
15
 
9
- This is the standard capabilities map advertised to clients,
10
- declaring support for list, cancel, and request operations.
16
+ Returns task capabilities as a first-class ServerCapabilities field,
17
+ declaring support for list, cancel, and request operations per SEP-1686.
18
+
19
+ Note: prompts/resources are passed via extra_data since the SDK types
20
+ don't include them yet (FastMCP supports them ahead of the spec).
11
21
  """
12
- return {
13
- "tasks": {
14
- "list": {},
15
- "cancel": {},
16
- "requests": {
17
- "tools": {"call": {}},
18
- "prompts": {"get": {}},
19
- "resources": {"read": {}},
20
- },
21
- }
22
- }
22
+ return ServerTasksCapability(
23
+ list=TasksListCapability(),
24
+ cancel=TasksCancelCapability(),
25
+ requests=ServerTasksRequestsCapability(
26
+ tools=TasksToolsCapability(call=TasksCallCapability()),
27
+ prompts={"get": {}}, # type: ignore[call-arg] # extra_data for forward compat
28
+ resources={"read": {}}, # type: ignore[call-arg] # extra_data for forward compat
29
+ ),
30
+ )
@@ -56,6 +56,7 @@ async def handle_tool_as_task(
56
56
  ctx = get_context()
57
57
  session_id = ctx.session_id
58
58
 
59
+ # Get Docket from ContextVar (set by Context.__aenter__ at request time)
59
60
  docket = _current_docket.get()
60
61
  if docket is None:
61
62
  raise McpError(
@@ -72,13 +73,15 @@ async def handle_tool_as_task(
72
73
  tool = await server.get_tool(tool_name)
73
74
 
74
75
  # Store task key mapping and creation timestamp in Redis for protocol handlers
75
- redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
76
- created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
76
+ task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
77
+ created_at_key = docket.key(
78
+ f"fastmcp:task:{session_id}:{server_task_id}:created_at"
79
+ )
77
80
  ttl_seconds = int(
78
81
  docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
79
82
  )
80
83
  async with docket.redis() as redis:
81
- await redis.set(redis_key, task_key, ex=ttl_seconds)
84
+ await redis.set(task_meta_key, task_key, ex=ttl_seconds)
82
85
  await redis.set(created_at_key, created_at, ex=ttl_seconds)
83
86
 
84
87
  # Send notifications/tasks/created per SEP-1686 (mandatory)
@@ -165,6 +168,7 @@ async def handle_prompt_as_task(
165
168
  ctx = get_context()
166
169
  session_id = ctx.session_id
167
170
 
171
+ # Get Docket from ContextVar (set by Context.__aenter__ at request time)
168
172
  docket = _current_docket.get()
169
173
  if docket is None:
170
174
  raise McpError(
@@ -181,13 +185,15 @@ async def handle_prompt_as_task(
181
185
  prompt = await server.get_prompt(prompt_name)
182
186
 
183
187
  # Store task key mapping and creation timestamp in Redis for protocol handlers
184
- redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
185
- created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
188
+ task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
189
+ created_at_key = docket.key(
190
+ f"fastmcp:task:{session_id}:{server_task_id}:created_at"
191
+ )
186
192
  ttl_seconds = int(
187
193
  docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
188
194
  )
189
195
  async with docket.redis() as redis:
190
- await redis.set(redis_key, task_key, ex=ttl_seconds)
196
+ await redis.set(task_meta_key, task_key, ex=ttl_seconds)
191
197
  await redis.set(created_at_key, created_at, ex=ttl_seconds)
192
198
 
193
199
  # Send notifications/tasks/created per SEP-1686 (mandatory)
@@ -272,12 +278,13 @@ async def handle_resource_as_task(
272
278
  ctx = get_context()
273
279
  session_id = ctx.session_id
274
280
 
281
+ # Get Docket from ContextVar (set by Context.__aenter__ at request time)
275
282
  docket = _current_docket.get()
276
283
  if docket is None:
277
284
  raise McpError(
278
285
  ErrorData(
279
286
  code=INTERNAL_ERROR,
280
- message="Background tasks require Docket",
287
+ message="Background tasks require a running FastMCP server context",
281
288
  )
282
289
  )
283
290
 
@@ -285,13 +292,15 @@ async def handle_resource_as_task(
285
292
  task_key = build_task_key(session_id, server_task_id, "resource", str(uri))
286
293
 
287
294
  # Store task key mapping and creation timestamp in Redis for protocol handlers
288
- redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
289
- created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
295
+ task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
296
+ created_at_key = docket.key(
297
+ f"fastmcp:task:{session_id}:{server_task_id}:created_at"
298
+ )
290
299
  ttl_seconds = int(
291
300
  docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
292
301
  )
293
302
  async with docket.redis() as redis:
294
- await redis.set(redis_key, task_key, ex=ttl_seconds)
303
+ await redis.set(task_meta_key, task_key, ex=ttl_seconds)
295
304
  await redis.set(created_at_key, created_at, ex=ttl_seconds)
296
305
 
297
306
  # Send notifications/tasks/created per SEP-1686 (mandatory)
@@ -77,10 +77,12 @@ async def tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskR
77
77
  )
78
78
 
79
79
  # Look up full task key and creation timestamp from Redis
80
- redis_key = f"fastmcp:task:{session_id}:{client_task_id}"
81
- created_at_key = f"fastmcp:task:{session_id}:{client_task_id}:created_at"
80
+ task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
81
+ created_at_key = docket.key(
82
+ f"fastmcp:task:{session_id}:{client_task_id}:created_at"
83
+ )
82
84
  async with docket.redis() as redis:
83
- task_key_bytes = await redis.get(redis_key)
85
+ task_key_bytes = await redis.get(task_meta_key)
84
86
  created_at_bytes = await redis.get(created_at_key)
85
87
 
86
88
  task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")
@@ -176,9 +178,9 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any:
176
178
  )
177
179
 
178
180
  # Look up full task key from Redis
179
- redis_key = f"fastmcp:task:{session_id}:{client_task_id}"
181
+ task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
180
182
  async with docket.redis() as redis:
181
- task_key_bytes = await redis.get(redis_key)
183
+ task_key_bytes = await redis.get(task_meta_key)
182
184
 
183
185
  task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")
184
186
 
@@ -309,10 +311,12 @@ async def tasks_cancel_handler(
309
311
  )
310
312
 
311
313
  # Look up full task key and creation timestamp from Redis
312
- redis_key = f"fastmcp:task:{session_id}:{client_task_id}"
313
- created_at_key = f"fastmcp:task:{session_id}:{client_task_id}:created_at"
314
+ task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
315
+ created_at_key = docket.key(
316
+ f"fastmcp:task:{session_id}:{client_task_id}:created_at"
317
+ )
314
318
  async with docket.redis() as redis:
315
- task_key_bytes = await redis.get(redis_key)
319
+ task_key_bytes = await redis.get(task_meta_key)
316
320
  created_at_bytes = await redis.get(created_at_key)
317
321
 
318
322
  task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")
@@ -70,7 +70,7 @@ async def subscribe_to_task_updates(
70
70
  )
71
71
 
72
72
  except Exception as e:
73
- logger.warning(f"Subscription task failed for {task_id}: {e}", exc_info=True)
73
+ logger.error(f"subscribe_to_task_updates failed for {task_id}: {e}")
74
74
 
75
75
 
76
76
  async def _send_status_notification(
@@ -101,8 +101,7 @@ async def _send_status_notification(
101
101
  key_parts = parse_task_key(task_key)
102
102
  session_id = key_parts["session_id"]
103
103
 
104
- # Retrieve createdAt timestamp from Redis
105
- created_at_key = f"fastmcp:task:{session_id}:{task_id}:created_at"
104
+ created_at_key = docket.key(f"fastmcp:task:{session_id}:{task_id}:created_at")
106
105
  async with docket.redis() as redis:
107
106
  created_at_bytes = await redis.get(created_at_key)
108
107
 
@@ -175,8 +174,7 @@ async def _send_progress_notification(
175
174
  key_parts = parse_task_key(task_key)
176
175
  session_id = key_parts["session_id"]
177
176
 
178
- # Retrieve createdAt timestamp from Redis
179
- created_at_key = f"fastmcp:task:{session_id}:{task_id}:created_at"
177
+ created_at_key = docket.key(f"fastmcp:task:{session_id}:{task_id}:created_at")
180
178
  async with docket.redis() as redis:
181
179
  created_at_bytes = await redis.get(created_at_key)
182
180
 
fastmcp/settings.py CHANGED
@@ -392,6 +392,21 @@ class Settings(BaseSettings):
392
392
  ),
393
393
  ] = True
394
394
 
395
+ check_for_updates: Annotated[
396
+ Literal["stable", "prerelease", "off"],
397
+ Field(
398
+ description=inspect.cleandoc(
399
+ """
400
+ Controls update checking when displaying the CLI banner.
401
+ - "stable": Check for stable releases only (default)
402
+ - "prerelease": Also check for pre-release versions (alpha, beta, rc)
403
+ - "off": Disable update checking entirely
404
+ Set via FASTMCP_CHECK_FOR_UPDATES environment variable.
405
+ """
406
+ ),
407
+ ),
408
+ ] = "stable"
409
+
395
410
  @property
396
411
  def server_auth_class(self) -> AuthProvider | None:
397
412
  from fastmcp.utilities.types import get_cached_typeadapter
fastmcp/utilities/cli.py CHANGED
@@ -17,6 +17,7 @@ from fastmcp.utilities.logging import get_logger
17
17
  from fastmcp.utilities.mcp_server_config import MCPServerConfig
18
18
  from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
19
19
  from fastmcp.utilities.types import get_cached_typeadapter
20
+ from fastmcp.utilities.version_check import check_for_newer_version
20
21
 
21
22
  if TYPE_CHECKING:
22
23
  from fastmcp import FastMCP
@@ -200,6 +201,9 @@ LOGO_ASCII_4 = (
200
201
  def log_server_banner(server: FastMCP[Any]) -> None:
201
202
  """Creates and logs a formatted banner with server information and logo."""
202
203
 
204
+ # Check for updates (non-blocking, fails silently)
205
+ newer_version = check_for_newer_version()
206
+
203
207
  # Create the logo text
204
208
  # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
205
209
  logo_text = Text.from_ansi(LOGO_ASCII_4, no_wrap=True)
@@ -231,8 +235,10 @@ def log_server_banner(server: FastMCP[Any]) -> None:
231
235
 
232
236
  # v3 notice banner (shown below main panel)
233
237
  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"
238
+ v3_line2 = Text.assemble(
239
+ ("Pin ", "dim"),
240
+ ("`fastmcp < 3`", "dim bold"),
241
+ (" in production, then upgrade when you're ready.", "dim"),
236
242
  )
237
243
  v3_notice = Panel(
238
244
  Group(Align.center(v3_line1), Align.center(v3_line2)),
@@ -250,5 +256,26 @@ def log_server_banner(server: FastMCP[Any]) -> None:
250
256
  )
251
257
 
252
258
  console = Console(stderr=True)
253
- # Center both panels
254
- console.print(Group("\n", Align.center(panel), Align.center(v3_notice), "\n"))
259
+
260
+ # Build output elements
261
+ output_elements: list[Align | Panel | str] = ["\n", Align.center(panel)]
262
+ output_elements.append(Align.center(v3_notice))
263
+
264
+ # Add update notice if a newer version is available (shown last for visibility)
265
+ if newer_version:
266
+ update_line1 = Text.assemble(
267
+ ("🎉 Update available: ", "bold"),
268
+ (newer_version, "bold green"),
269
+ )
270
+ update_line2 = Text("Run: pip install --upgrade fastmcp", style="dim")
271
+ update_notice = Panel(
272
+ Group(Align.center(update_line1), Align.center(update_line2)),
273
+ border_style="blue",
274
+ padding=(0, 2),
275
+ width=80,
276
+ )
277
+ output_elements.append(Align.center(update_notice))
278
+
279
+ output_elements.append("\n")
280
+
281
+ console.print(Group(*output_elements))
@@ -3,6 +3,130 @@ from __future__ import annotations
3
3
  from collections import defaultdict
4
4
  from typing import Any
5
5
 
6
+ from jsonref import JsonRefError, replace_refs
7
+
8
+
9
+ def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]:
10
+ """Resolve all $ref references in a JSON schema by inlining definitions.
11
+
12
+ This function resolves $ref references that point to $defs, replacing them
13
+ with the actual definition content while preserving sibling keywords (like
14
+ description, default, examples) that Pydantic places alongside $ref.
15
+
16
+ This is necessary because some MCP clients (e.g., VS Code Copilot) don't
17
+ properly handle $ref in tool input schemas.
18
+
19
+ For self-referencing/circular schemas where full dereferencing is not possible,
20
+ this function falls back to resolving only the root-level $ref while preserving
21
+ $defs for nested references.
22
+
23
+ Args:
24
+ schema: JSON schema dict that may contain $ref references
25
+
26
+ Returns:
27
+ A new schema dict with $ref resolved where possible and $defs removed
28
+ when no longer needed
29
+
30
+ Example:
31
+ >>> schema = {
32
+ ... "$defs": {"Category": {"enum": ["a", "b"], "type": "string"}},
33
+ ... "properties": {"cat": {"$ref": "#/$defs/Category", "default": "a"}}
34
+ ... }
35
+ >>> resolved = dereference_refs(schema)
36
+ >>> # Result: {"properties": {"cat": {"enum": ["a", "b"], "type": "string", "default": "a"}}}
37
+ """
38
+ try:
39
+ # Use jsonref to resolve all $ref references
40
+ # proxies=False returns plain dicts (not proxy objects)
41
+ # lazy_load=False resolves immediately
42
+ dereferenced = replace_refs(schema, proxies=False, lazy_load=False)
43
+
44
+ # Merge sibling keywords that were lost during dereferencing
45
+ # Pydantic puts description, default, examples as siblings to $ref
46
+ defs = schema.get("$defs", {})
47
+ merged = _merge_ref_siblings(schema, dereferenced, defs)
48
+ # Type assertion: top-level schema is always a dict
49
+ assert isinstance(merged, dict)
50
+ dereferenced = merged
51
+
52
+ # Remove $defs since all references have been resolved
53
+ if "$defs" in dereferenced:
54
+ dereferenced = {k: v for k, v in dereferenced.items() if k != "$defs"}
55
+
56
+ return dereferenced
57
+
58
+ except JsonRefError:
59
+ # Self-referencing/circular schemas can't be fully dereferenced
60
+ # Fall back to resolving only root-level $ref (for MCP spec compliance)
61
+ return resolve_root_ref(schema)
62
+
63
+
64
+ def _merge_ref_siblings(
65
+ original: Any,
66
+ dereferenced: Any,
67
+ defs: dict[str, Any],
68
+ visited: set[str] | None = None,
69
+ ) -> Any:
70
+ """Merge sibling keywords from original $ref nodes into dereferenced schema.
71
+
72
+ When jsonref resolves $ref, it replaces the entire node with the referenced
73
+ definition, losing any sibling keywords like description, default, or examples.
74
+ This function walks both trees in parallel and merges those siblings back.
75
+
76
+ Args:
77
+ original: The original schema with $ref and potential siblings
78
+ dereferenced: The schema after jsonref processing
79
+ defs: The $defs from the original schema, for looking up referenced definitions
80
+ visited: Set of definition names already being processed (prevents cycles)
81
+
82
+ Returns:
83
+ The dereferenced schema with sibling keywords restored
84
+ """
85
+ if visited is None:
86
+ visited = set()
87
+
88
+ if isinstance(original, dict) and isinstance(dereferenced, dict):
89
+ # Check if original had a $ref
90
+ if "$ref" in original:
91
+ ref = original["$ref"]
92
+ siblings = {k: v for k, v in original.items() if k not in ("$ref", "$defs")}
93
+
94
+ # Look up the referenced definition to process its nested siblings
95
+ if isinstance(ref, str) and ref.startswith("#/$defs/"):
96
+ def_name = ref.split("/")[-1]
97
+ # Prevent infinite recursion on circular references
98
+ if def_name in defs and def_name not in visited:
99
+ # Recursively process the definition's content for nested siblings
100
+ dereferenced = _merge_ref_siblings(
101
+ defs[def_name], dereferenced, defs, visited | {def_name}
102
+ )
103
+
104
+ if siblings:
105
+ # Merge local siblings, which take precedence
106
+ merged = dict(dereferenced)
107
+ merged.update(siblings)
108
+ return merged
109
+ return dereferenced
110
+
111
+ # Recurse into nested structures
112
+ result = {}
113
+ for key, value in dereferenced.items():
114
+ if key in original:
115
+ result[key] = _merge_ref_siblings(original[key], value, defs, visited)
116
+ else:
117
+ result[key] = value
118
+ return result
119
+
120
+ elif isinstance(original, list) and isinstance(dereferenced, list):
121
+ # Process list items in parallel
122
+ min_len = min(len(original), len(dereferenced))
123
+ return [
124
+ _merge_ref_siblings(o, d, defs, visited)
125
+ for o, d in zip(original[:min_len], dereferenced[:min_len], strict=False)
126
+ ] + dereferenced[min_len:]
127
+
128
+ return dereferenced
129
+
6
130
 
7
131
  def resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]:
8
132
  """Resolve $ref at root level to meet MCP spec requirements.
@@ -43,7 +167,7 @@ def resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]:
43
167
  return schema
44
168
 
45
169
 
46
- def _prune_param(schema: dict, param: str) -> dict:
170
+ def _prune_param(schema: dict[str, Any], param: str) -> dict[str, Any]:
47
171
  """Return a new schema with *param* removed from `properties`, `required`,
48
172
  and (if no longer referenced) `$defs`.
49
173
  """
@@ -65,11 +189,11 @@ def _prune_param(schema: dict, param: str) -> dict:
65
189
 
66
190
 
67
191
  def _single_pass_optimize(
68
- schema: dict,
192
+ schema: dict[str, Any],
69
193
  prune_titles: bool = False,
70
194
  prune_additional_properties: bool = False,
71
195
  prune_defs: bool = True,
72
- ) -> dict:
196
+ ) -> dict[str, Any]:
73
197
  """
74
198
  Optimize JSON schemas in a single traversal for better performance.
75
199
 
@@ -238,12 +362,12 @@ def _single_pass_optimize(
238
362
 
239
363
 
240
364
  def compress_schema(
241
- schema: dict,
365
+ schema: dict[str, Any],
242
366
  prune_params: list[str] | None = None,
243
367
  prune_defs: bool = True,
244
368
  prune_additional_properties: bool = True,
245
369
  prune_titles: bool = False,
246
- ) -> dict:
370
+ ) -> dict[str, Any]:
247
371
  """
248
372
  Remove the given parameters from the schema.
249
373
 
@@ -0,0 +1,153 @@
1
+ """Version checking utilities for FastMCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+ from packaging.version import Version
11
+
12
+ from fastmcp.utilities.logging import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+ PYPI_URL = "https://pypi.org/pypi/fastmcp/json"
17
+ CACHE_TTL_SECONDS = 60 * 60 * 12 # 12 hours
18
+ REQUEST_TIMEOUT_SECONDS = 2.0
19
+
20
+
21
+ def _get_cache_path(include_prereleases: bool = False) -> Path:
22
+ """Get the path to the version cache file."""
23
+ import fastmcp
24
+
25
+ suffix = "_prerelease" if include_prereleases else ""
26
+ return fastmcp.settings.home / f"version_cache{suffix}.json"
27
+
28
+
29
+ def _read_cache(include_prereleases: bool = False) -> tuple[str | None, float]:
30
+ """Read cached version info.
31
+
32
+ Returns:
33
+ Tuple of (cached_version, cache_timestamp) or (None, 0) if no cache.
34
+ """
35
+ cache_path = _get_cache_path(include_prereleases)
36
+ if not cache_path.exists():
37
+ return None, 0
38
+
39
+ try:
40
+ data = json.loads(cache_path.read_text())
41
+ return data.get("latest_version"), data.get("timestamp", 0)
42
+ except (json.JSONDecodeError, OSError):
43
+ return None, 0
44
+
45
+
46
+ def _write_cache(latest_version: str, include_prereleases: bool = False) -> None:
47
+ """Write version info to cache."""
48
+ cache_path = _get_cache_path(include_prereleases)
49
+ try:
50
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
51
+ cache_path.write_text(
52
+ json.dumps({"latest_version": latest_version, "timestamp": time.time()})
53
+ )
54
+ except OSError:
55
+ # Silently ignore cache write failures
56
+ pass
57
+
58
+
59
+ def _fetch_latest_version(include_prereleases: bool = False) -> str | None:
60
+ """Fetch the latest version from PyPI.
61
+
62
+ Args:
63
+ include_prereleases: If True, include pre-release versions (alpha, beta, rc).
64
+
65
+ Returns:
66
+ The latest version string, or None if the fetch failed.
67
+ """
68
+ try:
69
+ response = httpx.get(PYPI_URL, timeout=REQUEST_TIMEOUT_SECONDS)
70
+ response.raise_for_status()
71
+ data = response.json()
72
+
73
+ releases = data.get("releases", {})
74
+ if not releases:
75
+ return None
76
+
77
+ versions = []
78
+ for version_str in releases:
79
+ try:
80
+ v = Version(version_str)
81
+ # Skip prereleases if not requested
82
+ if not include_prereleases and v.is_prerelease:
83
+ continue
84
+ versions.append(v)
85
+ except ValueError:
86
+ logger.debug(f"Skipping invalid version string: {version_str}")
87
+ continue
88
+
89
+ if not versions:
90
+ return None
91
+
92
+ return str(max(versions))
93
+
94
+ except (httpx.HTTPError, json.JSONDecodeError, KeyError):
95
+ return None
96
+
97
+
98
+ def get_latest_version(include_prereleases: bool = False) -> str | None:
99
+ """Get the latest version of FastMCP from PyPI, using cache when available.
100
+
101
+ Args:
102
+ include_prereleases: If True, include pre-release versions.
103
+
104
+ Returns:
105
+ The latest version string, or None if unavailable.
106
+ """
107
+ # Check cache first
108
+ cached_version, cache_timestamp = _read_cache(include_prereleases)
109
+ if cached_version and (time.time() - cache_timestamp) < CACHE_TTL_SECONDS:
110
+ return cached_version
111
+
112
+ # Fetch from PyPI
113
+ latest_version = _fetch_latest_version(include_prereleases)
114
+
115
+ # Update cache if we got a valid version
116
+ if latest_version:
117
+ _write_cache(latest_version, include_prereleases)
118
+ return latest_version
119
+
120
+ # Return stale cache if available
121
+ return cached_version
122
+
123
+
124
+ def check_for_newer_version() -> str | None:
125
+ """Check if a newer version of FastMCP is available.
126
+
127
+ Returns:
128
+ The latest version string if newer than current, None otherwise.
129
+ """
130
+ import fastmcp
131
+
132
+ setting = fastmcp.settings.check_for_updates
133
+ if setting == "off":
134
+ return None
135
+
136
+ include_prereleases = setting == "prerelease"
137
+ latest_version = get_latest_version(include_prereleases)
138
+ if not latest_version:
139
+ return None
140
+
141
+ try:
142
+ current = Version(fastmcp.__version__)
143
+ latest = Version(latest_version)
144
+
145
+ if latest > current:
146
+ return latest_version
147
+ except ValueError:
148
+ logger.debug(
149
+ f"Could not compare versions: current={fastmcp.__version__!r}, "
150
+ f"latest={latest_version!r}"
151
+ )
152
+
153
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.14.2
3
+ Version: 2.14.4
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
@@ -22,13 +22,15 @@ Requires-Dist: authlib>=1.6.5
22
22
  Requires-Dist: cyclopts>=4.0.0
23
23
  Requires-Dist: exceptiongroup>=1.2.2
24
24
  Requires-Dist: httpx>=0.28.1
25
+ Requires-Dist: jsonref>=1.1.0
25
26
  Requires-Dist: jsonschema-path>=0.3.4
26
27
  Requires-Dist: mcp<2.0,>=1.24.0
27
28
  Requires-Dist: openapi-pydantic>=0.5.1
29
+ Requires-Dist: packaging>=20.0
28
30
  Requires-Dist: platformdirs>=4.0.0
29
31
  Requires-Dist: py-key-value-aio[disk,keyring,memory]<0.4.0,>=0.3.0
30
32
  Requires-Dist: pydantic[email]>=2.11.7
31
- Requires-Dist: pydocket>=0.16.3
33
+ Requires-Dist: pydocket<0.17.0,>=0.16.6
32
34
  Requires-Dist: pyperclip>=1.9.0
33
35
  Requires-Dist: python-dotenv>=1.1.0
34
36
  Requires-Dist: rich>=13.9.4
@@ -3,10 +3,10 @@ fastmcp/dependencies.py,sha256=Un5S30WHJbAiIdjVjEeaQC7UcEVEkkyjf4EF7l4FYq0,513
3
3
  fastmcp/exceptions.py,sha256=-krEavxwddQau6T7MESCR4VjKNLfP9KHJrU1p3y72FU,744
4
4
  fastmcp/mcp_config.py,sha256=YXZ0piljrxFgPYEwYSwPw6IiPwU3Cwp2VzlT9CWxutc,11397
5
5
  fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- fastmcp/settings.py,sha256=565ICavv2ms1lCvGV4XS6VXIYJZyB0os3Ifsr03A6YA,13307
6
+ fastmcp/settings.py,sha256=IL3r6kyGzy3WW4evL4BwDfBGNNArbCnwnGmKlOHTr8Y,13888
7
7
  fastmcp/cli/__init__.py,sha256=Bo7WQWPBRQ6fqbYYPfbadefpXgl2h9gkdMaqTazGWyw,49
8
8
  fastmcp/cli/__main__.py,sha256=cGU_smvfctQI9xEY13u7tTEwwUI4AUieikXXA7ykYhA,69
9
- fastmcp/cli/cli.py,sha256=HQ7MpR_7sB-bgQKG1XOd8epS2RIv_CqdYdHT8mZLszI,28242
9
+ fastmcp/cli/cli.py,sha256=Bedska9lyo7NuMUb4G0hvBK3OzG7BMTnA756s72rdq8,28664
10
10
  fastmcp/cli/run.py,sha256=HeaiHYcVY17JpHg4UjnIHkP5ttU0PNd1bZIL3brif8A,7047
11
11
  fastmcp/cli/tasks.py,sha256=B57vy76d3ybdi4wmlODRCCFrte1GmhLKqYixzRkGUuw,3791
12
12
  fastmcp/cli/install/__init__.py,sha256=FUrwjMVaxONgz1qO7suzJNz1xosRfR3TOHlr3Z77JXA,797
@@ -25,10 +25,10 @@ fastmcp/client/oauth_callback.py,sha256=3xqL5_HD1QS9eGfw31HzoVF94QQelq_0TTqS7qWD
25
25
  fastmcp/client/progress.py,sha256=WjLLDbUKMsx8DK-fqO7AGsXb83ak-6BMrLvzzznGmcI,1043
26
26
  fastmcp/client/roots.py,sha256=Uap1RSr3uEeQRZTHkEttkhTI2fOA8IeDcRSggtZp9aY,2568
27
27
  fastmcp/client/tasks.py,sha256=zjiTfvjU9NaA4e3XTBGHsqvSfBRR19UqZMIUhJ_nQTo,19480
28
- fastmcp/client/transports.py,sha256=ZRxHTgNYK6_D_2GNa15cfGMNP-naZ0A9q8oc2jfgX8A,43231
28
+ fastmcp/client/transports.py,sha256=iFoivucnJb0LoDVu9HbEgWta3KcxRjM3arR4ebZOZco,43291
29
29
  fastmcp/client/auth/__init__.py,sha256=4DNsfp4iaQeBcpds0JDdMn6Mmfud44stWLsret0sVKY,91
30
30
  fastmcp/client/auth/bearer.py,sha256=MFEFqcH6u_V86msYiOsEFKN5ks1V9BnBNiPsPLHUTqo,399
31
- fastmcp/client/auth/oauth.py,sha256=8B1HTPoPhEFQUZBfuhR6jqq4CHu6BDATVowC3ayZmg8,12513
31
+ fastmcp/client/auth/oauth.py,sha256=PXtWFFSqR29QZ_ZYk74EIRHdj_qOGP2yerXb0HDw2ns,12745
32
32
  fastmcp/client/sampling/__init__.py,sha256=jaquyp7c5lz4mczv0d5Skl153uWrnXVcS4qCmbjLKRY,2208
33
33
  fastmcp/client/sampling/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  fastmcp/client/sampling/handlers/anthropic.py,sha256=LjxTYzIWOnbJwJDHJOkRybW0dXBcyt2c7B4sCaG3uLM,14318
@@ -61,19 +61,19 @@ fastmcp/resources/resource_manager.py,sha256=yG3EieKY9DqIcYTIFJkSJlRoXeffV6mTOnW
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
- fastmcp/server/context.py,sha256=nd0bME6I7aB9RlKiESNBay97P8yDHJJ-IcjVCbkc7bc,42256
64
+ fastmcp/server/context.py,sha256=vivwwI4u7RuaYMivQ0IlqqHDQxZo682ZMckt-5muC3A,43187
65
65
  fastmcp/server/dependencies.py,sha256=gRc60PhEvna9rlqMW-ZlYNszPlUeEeOWT5winYGNH2A,20928
66
66
  fastmcp/server/elicitation.py,sha256=CmHi_SERmhEcNjwnM90_HGihUKlCM3RPGHI0uns2t7M,17912
67
67
  fastmcp/server/event_store.py,sha256=ZiBbrUQHw9--G8lzK1qLZmUAF2le2XchFen4pGbFKsE,6170
68
- fastmcp/server/http.py,sha256=_HjMSYWH8mfKugDODU4iV0AhKDU2VRc40tS56L6i-_s,12737
69
- fastmcp/server/low_level.py,sha256=o3jDf5SuZBQeurhLWRzaSVCnvrmaKMH_w-TbHk6BuZ4,7963
68
+ fastmcp/server/http.py,sha256=mQnb9moDqgzqgo-H30Qii6EcOSMhPjrh4rL3zr5ES4g,12469
69
+ fastmcp/server/low_level.py,sha256=afTlucPVSOaG9N68D8CGZh4oU1tc9g_IpLfX7crZB4o,9000
70
70
  fastmcp/server/proxy.py,sha256=bsgVkcdlRtVK3bB4EeVKrq4PLjIoUvWN_hgzr1hq8yE,26837
71
- fastmcp/server/server.py,sha256=3RmmJT2vM-ycXces_L6ORSQqlQvzMVITu37dCgqp-Ao,121385
71
+ fastmcp/server/server.py,sha256=HCUJBE0E47gOx_f1o6XJn5U-JChc_wlKtFK5EIDnNjc,121332
72
72
  fastmcp/server/auth/__init__.py,sha256=MTZvDKEUMqjs9-raRN0h8Zjx8pWFXs_iSRbB1UqBUqU,527
73
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=c5BAyT07t3vZtfD9wCoN4f8hnSD1F5e0kCMqlUuO_00,93364
76
+ fastmcp/server/auth/oauth_proxy.py,sha256=pEvYQAhTRT6eiIkK4eG5VnlOIr7fnHSEDO803ULCP5Q,94785
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
@@ -110,31 +110,32 @@ fastmcp/server/sampling/__init__.py,sha256=u9jDHSE_yz6kTzbFqIOXqnM0PfIAiP-peAjHJ
110
110
  fastmcp/server/sampling/run.py,sha256=1FIg9TMcvilQcgW0i00xlpgd7Yz6b814ntG3ihtFo6g,10469
111
111
  fastmcp/server/sampling/sampling_tool.py,sha256=YltN7-NcMXUk6cFbuOQmuJ980bmQyh83qgFo0TogP5Q,3311
112
112
  fastmcp/server/tasks/__init__.py,sha256=VizXvmXgA3SvrApQ6PSz4z1TPA9B6uROvmWeGSYOJ0I,530
113
- fastmcp/server/tasks/capabilities.py,sha256=-8QMBjs6HZuQdUNmOrNEBvJs-opGptIyxOODU0TGGFE,574
113
+ fastmcp/server/tasks/capabilities.py,sha256=ut7PrKFUPu9FAO9IzqmoS2iQ7ByQWZOAqsMXxyS34dI,1074
114
114
  fastmcp/server/tasks/config.py,sha256=msPkUuxnZKuqSj21Eh8m5Cwq0htwUzTCeoWsnbvKGkk,3006
115
115
  fastmcp/server/tasks/converters.py,sha256=ON7c8gOMjBYiQoyk_vkymI8J01ccoYzizDwtgIIqIZQ,6701
116
- fastmcp/server/tasks/handlers.py,sha256=8wiDiP6tHj8E3iMEIpjZmoiY_k1wgQg-F59KwGk56X8,12489
116
+ fastmcp/server/tasks/handlers.py,sha256=1KTyfPgpQ-6YRfiK3s10sqa2pkRB0tR6ZZzb55KLZDk,12884
117
117
  fastmcp/server/tasks/keys.py,sha256=w9diycj0N6ViVqe6stxUS9vg2H94bl_614Bu5kNRM-k,3011
118
- fastmcp/server/tasks/protocol.py,sha256=g97D4k1U8ua_UBTyoqFXcPp5rf6KvuiY5d6mx5KMIPY,12222
119
- fastmcp/server/tasks/subscriptions.py,sha256=iehPO2zx80aRIqKHCFj9kuR5NVMqYSkIepMXBifQFWw,6692
118
+ fastmcp/server/tasks/protocol.py,sha256=1wmpubpLb5URzz9JMrPSKmoRRtvrYJ_SW16DROAvXQo,12350
119
+ fastmcp/server/tasks/subscriptions.py,sha256=cNJptdgkofJ6Gg8ae92MAkr95aZewxl--l8BE1_ZJ1U,6615
120
120
  fastmcp/tools/__init__.py,sha256=XGcaMkBgwr-AHzbNjyjdb3ATgp5TQ0wzSq0nsrBD__E,201
121
121
  fastmcp/tools/tool.py,sha256=_l0HEnuTyYxm_xNWYxO2seRnzb6NunvjnEsWQIeKBDY,23394
122
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=qjZtF9LvReKDOaLDIzcWWErMpWUBA8-Uvml8H4upuqs,11515
126
+ fastmcp/utilities/cli.py,sha256=QPYbVJnH0oNmGbo-vg-3nhqr-zJYSxJfsF7r2n9uQXc,12455
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=H8RNucfulnXqYjCzRrlaWCBfToHmJGc7M32VJu5q7Eo,10587
131
+ fastmcp/utilities/json_schema.py,sha256=rhSub4bxP_ACW8VJu1SoQCbL32wQNV8PUJlzt1uUR5g,15701
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
135
135
  fastmcp/utilities/tests.py,sha256=VIsYPpk07tXvE02yK_neBUeZgu5YtbUlK6JJNzU-6lQ,9229
136
136
  fastmcp/utilities/types.py,sha256=7c56m736JjbKY-YP7RLWPZcsW5Z7mikpByKaDQ5IJwg,17586
137
137
  fastmcp/utilities/ui.py,sha256=gcnha7Vj4xEBxdrS83EZlKpN_43AQzcgiZFEvkTqzqg,14252
138
+ fastmcp/utilities/version_check.py,sha256=zjY2HaSW4f09Gjun3V6TLyaeJC_ZPPf16VvQAdDigO8,4419
138
139
  fastmcp/utilities/mcp_server_config/__init__.py,sha256=hHBxEwRsrgN0Q-1bvj28X6UVGDpfG6dt3yfSBGsOY80,791
139
140
  fastmcp/utilities/mcp_server_config/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
141
  fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py,sha256=1B3J7sRR0GcOW6FcSNNTTTOtEePNhUKc7Y0xEDk-wao,15497
@@ -153,8 +154,8 @@ fastmcp/utilities/openapi/json_schema_converter.py,sha256=PxaYpgHBsdDTT0XSP6s4RZ
153
154
  fastmcp/utilities/openapi/models.py,sha256=-kfndwZSe92tVtKAgOuFn5rk1tN7oydCZKtLOEMEalA,2805
154
155
  fastmcp/utilities/openapi/parser.py,sha256=qsa68Ro1c8ov77kdEP20IwZqD74E4IGKjtfeIkn3HdE,34338
155
156
  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,,
157
+ fastmcp-2.14.4.dist-info/METADATA,sha256=R9PnAzkKHt-74_BnjfswvaVfvKdrjLvizrwLj2SUgJA,20840
158
+ fastmcp-2.14.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
159
+ fastmcp-2.14.4.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
160
+ fastmcp-2.14.4.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
161
+ fastmcp-2.14.4.dist-info/RECORD,,