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 +9 -0
- fastmcp/client/auth/oauth.py +4 -1
- fastmcp/client/transports.py +20 -20
- fastmcp/server/auth/oauth_proxy.py +45 -10
- fastmcp/server/context.py +28 -4
- fastmcp/server/http.py +1 -7
- fastmcp/server/low_level.py +25 -0
- fastmcp/server/server.py +7 -9
- fastmcp/server/tasks/capabilities.py +24 -16
- fastmcp/server/tasks/handlers.py +19 -10
- fastmcp/server/tasks/protocol.py +12 -8
- fastmcp/server/tasks/subscriptions.py +3 -5
- fastmcp/settings.py +15 -0
- fastmcp/utilities/cli.py +31 -4
- fastmcp/utilities/json_schema.py +129 -5
- fastmcp/utilities/version_check.py +153 -0
- {fastmcp-2.14.2.dist-info → fastmcp-2.14.4.dist-info}/METADATA +4 -2
- {fastmcp-2.14.2.dist-info → fastmcp-2.14.4.dist-info}/RECORD +21 -20
- {fastmcp-2.14.2.dist-info → fastmcp-2.14.4.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.2.dist-info → fastmcp-2.14.4.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.2.dist-info → fastmcp-2.14.4.dist-info}/licenses/LICENSE +0 -0
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(
|
fastmcp/client/auth/oauth.py
CHANGED
|
@@ -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=
|
|
114
|
+
ttl=60 * 60 * 24 * 365, # 1 year
|
|
112
115
|
)
|
|
113
116
|
|
|
114
117
|
@override
|
fastmcp/client/transports.py
CHANGED
|
@@ -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
|
-
#
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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=
|
|
1270
|
-
|
|
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=
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
fastmcp/server/low_level.py
CHANGED
|
@@ -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
|
|
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
|
|
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() ->
|
|
7
|
-
"""Return the SEP-1686 task capabilities
|
|
13
|
+
def get_task_capabilities() -> ServerTasksCapability:
|
|
14
|
+
"""Return the SEP-1686 task capabilities.
|
|
8
15
|
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
)
|
fastmcp/server/tasks/handlers.py
CHANGED
|
@@ -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
|
-
|
|
76
|
-
created_at_key =
|
|
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(
|
|
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
|
-
|
|
185
|
-
created_at_key =
|
|
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(
|
|
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
|
|
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
|
-
|
|
289
|
-
created_at_key =
|
|
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(
|
|
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)
|
fastmcp/server/tasks/protocol.py
CHANGED
|
@@ -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
|
-
|
|
81
|
-
created_at_key =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
313
|
-
created_at_key =
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
254
|
-
|
|
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))
|
fastmcp/utilities/json_schema.py
CHANGED
|
@@ -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.
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
69
|
-
fastmcp/server/low_level.py,sha256=
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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=
|
|
119
|
-
fastmcp/server/tasks/subscriptions.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
157
|
-
fastmcp-2.14.
|
|
158
|
-
fastmcp-2.14.
|
|
159
|
-
fastmcp-2.14.
|
|
160
|
-
fastmcp-2.14.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|