fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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.
Files changed (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,253 @@
1
+ """Client utilities for discovering and downloading skills from MCP servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from fastmcp.client import Client
12
+
13
+
14
+ @dataclass
15
+ class SkillSummary:
16
+ """Summary information about a skill available on a server."""
17
+
18
+ name: str
19
+ description: str
20
+ uri: str
21
+
22
+
23
+ @dataclass
24
+ class SkillFile:
25
+ """Information about a file within a skill."""
26
+
27
+ path: str
28
+ size: int
29
+ hash: str
30
+
31
+
32
+ @dataclass
33
+ class SkillManifest:
34
+ """Full manifest of a skill including all files."""
35
+
36
+ name: str
37
+ files: list[SkillFile]
38
+
39
+
40
+ async def list_skills(client: Client) -> list[SkillSummary]:
41
+ """List all available skills from an MCP server.
42
+
43
+ Discovers skills by finding resources with URIs matching the
44
+ `skill://{name}/SKILL.md` pattern.
45
+
46
+ Args:
47
+ client: Connected FastMCP client
48
+
49
+ Returns:
50
+ List of SkillSummary objects with name, description, and URI
51
+
52
+ Example:
53
+ ```python
54
+ from fastmcp import Client
55
+ from fastmcp.utilities.skills import list_skills
56
+
57
+ async with Client("http://skills-server/mcp") as client:
58
+ skills = await list_skills(client)
59
+ for skill in skills:
60
+ print(f"{skill.name}: {skill.description}")
61
+ ```
62
+ """
63
+ resources = await client.list_resources()
64
+ skills = []
65
+
66
+ for resource in resources:
67
+ uri = str(resource.uri)
68
+ # Match skill://{name}/SKILL.md pattern
69
+ if uri.startswith("skill://") and uri.endswith("/SKILL.md"):
70
+ # Extract skill name from URI
71
+ path_part = uri[len("skill://") :]
72
+ name = path_part.rsplit("/", 1)[0]
73
+ skills.append(
74
+ SkillSummary(
75
+ name=name,
76
+ description=resource.description or "",
77
+ uri=uri,
78
+ )
79
+ )
80
+
81
+ return skills
82
+
83
+
84
+ async def get_skill_manifest(client: Client, skill_name: str) -> SkillManifest:
85
+ """Get the manifest for a specific skill.
86
+
87
+ Args:
88
+ client: Connected FastMCP client
89
+ skill_name: Name of the skill
90
+
91
+ Returns:
92
+ SkillManifest with file listing
93
+
94
+ Raises:
95
+ ValueError: If manifest cannot be read or parsed
96
+ """
97
+ manifest_uri = f"skill://{skill_name}/_manifest"
98
+ result = await client.read_resource(manifest_uri)
99
+
100
+ if not result:
101
+ raise ValueError(f"Could not read manifest for skill: {skill_name}")
102
+
103
+ content = result[0]
104
+ if hasattr(content, "text"):
105
+ try:
106
+ manifest_data = json.loads(content.text)
107
+ except json.JSONDecodeError as e:
108
+ raise ValueError(f"Invalid manifest JSON for skill: {skill_name}") from e
109
+ else:
110
+ raise ValueError(f"Unexpected manifest format for skill: {skill_name}")
111
+
112
+ try:
113
+ return SkillManifest(
114
+ name=manifest_data["skill"],
115
+ files=[
116
+ SkillFile(path=f["path"], size=f["size"], hash=f["hash"])
117
+ for f in manifest_data["files"]
118
+ ],
119
+ )
120
+ except (KeyError, TypeError) as e:
121
+ raise ValueError(f"Invalid manifest format for skill: {skill_name}") from e
122
+
123
+
124
+ async def download_skill(
125
+ client: Client,
126
+ skill_name: str,
127
+ target_dir: str | Path,
128
+ *,
129
+ overwrite: bool = False,
130
+ ) -> Path:
131
+ """Download a skill and all its files to a local directory.
132
+
133
+ Creates a subdirectory named after the skill containing all files.
134
+
135
+ Args:
136
+ client: Connected FastMCP client
137
+ skill_name: Name of the skill to download
138
+ target_dir: Directory where skill folder will be created
139
+ overwrite: If True, overwrite existing skill directory. If False
140
+ (default), raise FileExistsError if directory exists.
141
+
142
+ Returns:
143
+ Path to the downloaded skill directory
144
+
145
+ Raises:
146
+ ValueError: If skill cannot be found or downloaded
147
+ FileExistsError: If skill directory exists and overwrite=False
148
+
149
+ Example:
150
+ ```python
151
+ from fastmcp import Client
152
+ from fastmcp.utilities.skills import download_skill
153
+
154
+ async with Client("http://skills-server/mcp") as client:
155
+ skill_path = await download_skill(
156
+ client,
157
+ "pdf-processing",
158
+ "~/.claude/skills"
159
+ )
160
+ print(f"Downloaded to: {skill_path}")
161
+ ```
162
+ """
163
+ target_dir = Path(target_dir).expanduser().resolve()
164
+ skill_dir = target_dir / skill_name
165
+
166
+ # Check if directory exists
167
+ if skill_dir.exists() and not overwrite:
168
+ raise FileExistsError(
169
+ f"Skill directory already exists: {skill_dir}. "
170
+ "Use overwrite=True to replace."
171
+ )
172
+
173
+ # Get manifest to know what files to download
174
+ manifest = await get_skill_manifest(client, skill_name)
175
+
176
+ # Create skill directory
177
+ skill_dir.mkdir(parents=True, exist_ok=True)
178
+
179
+ # Download each file
180
+ for file_info in manifest.files:
181
+ # Security: reject absolute paths and paths that escape skill_dir
182
+ if Path(file_info.path).is_absolute():
183
+ continue
184
+ file_path = (skill_dir / file_info.path).resolve()
185
+ if not file_path.is_relative_to(skill_dir):
186
+ continue
187
+
188
+ file_uri = f"skill://{skill_name}/{file_info.path}"
189
+ result = await client.read_resource(file_uri)
190
+
191
+ if not result:
192
+ continue
193
+
194
+ content = result[0]
195
+
196
+ # Create parent directories if needed
197
+ file_path.parent.mkdir(parents=True, exist_ok=True)
198
+
199
+ # Write content
200
+ if hasattr(content, "text"):
201
+ file_path.write_text(content.text)
202
+ elif hasattr(content, "blob"):
203
+ # Handle base64-encoded binary content
204
+ import base64
205
+
206
+ file_path.write_bytes(base64.b64decode(content.blob))
207
+ else:
208
+ # Skip unknown content types
209
+ continue
210
+
211
+ return skill_dir
212
+
213
+
214
+ async def sync_skills(
215
+ client: Client,
216
+ target_dir: str | Path,
217
+ *,
218
+ overwrite: bool = False,
219
+ ) -> list[Path]:
220
+ """Download all available skills from a server.
221
+
222
+ Args:
223
+ client: Connected FastMCP client
224
+ target_dir: Directory where skill folders will be created
225
+ overwrite: If True, overwrite existing files
226
+
227
+ Returns:
228
+ List of paths to downloaded skill directories
229
+
230
+ Example:
231
+ ```python
232
+ from fastmcp import Client
233
+ from fastmcp.utilities.skills import sync_skills
234
+
235
+ async with Client("http://skills-server/mcp") as client:
236
+ paths = await sync_skills(client, "~/.claude/skills")
237
+ print(f"Downloaded {len(paths)} skills")
238
+ ```
239
+ """
240
+ skills = await list_skills(client)
241
+ downloaded = []
242
+
243
+ for skill in skills:
244
+ try:
245
+ path = await download_skill(
246
+ client, skill.name, target_dir, overwrite=overwrite
247
+ )
248
+ downloaded.append(path)
249
+ except FileExistsError:
250
+ # Skip existing skills when not overwriting
251
+ continue
252
+
253
+ return downloaded
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import copy
4
- import logging
5
4
  import multiprocessing
6
5
  import socket
7
6
  import time
@@ -12,7 +11,6 @@ from urllib.parse import parse_qs, urlparse
12
11
 
13
12
  import httpx
14
13
  import uvicorn
15
- from pytest import LogCaptureFixture
16
14
 
17
15
  from fastmcp import settings
18
16
  from fastmcp.client.auth.oauth import OAuth
@@ -224,20 +222,6 @@ async def run_server_async(
224
222
  await asyncio.wait_for(server_task, timeout=2.0)
225
223
 
226
224
 
227
- @contextmanager
228
- def caplog_for_fastmcp(
229
- caplog: LogCaptureFixture,
230
- ) -> Generator[LogCaptureFixture, None, None]:
231
- """Context manager to capture logs from FastMCP loggers even when propagation is disabled."""
232
- caplog.clear()
233
- logger = logging.getLogger("fastmcp")
234
- logger.addHandler(caplog.handler)
235
- try:
236
- yield caplog
237
- finally:
238
- logger.removeHandler(caplog.handler)
239
-
240
-
241
225
  class HeadlessOAuth(OAuth):
242
226
  """
243
227
  OAuth provider that bypasses browser interaction for testing.
@@ -0,0 +1,47 @@
1
+ """Timeout normalization utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+
7
+
8
+ def normalize_timeout_to_timedelta(
9
+ value: int | float | datetime.timedelta | None,
10
+ ) -> datetime.timedelta | None:
11
+ """Normalize a timeout value to a timedelta.
12
+
13
+ Args:
14
+ value: Timeout value as int/float (seconds), timedelta, or None
15
+
16
+ Returns:
17
+ timedelta if value provided, None otherwise
18
+ """
19
+ if value is None:
20
+ return None
21
+ if isinstance(value, datetime.timedelta):
22
+ return value
23
+ if isinstance(value, int | float):
24
+ return datetime.timedelta(seconds=float(value))
25
+ raise TypeError(f"Invalid timeout type: {type(value)}")
26
+
27
+
28
+ def normalize_timeout_to_seconds(
29
+ value: int | float | datetime.timedelta | None,
30
+ ) -> float | None:
31
+ """Normalize a timeout value to seconds (float).
32
+
33
+ Args:
34
+ value: Timeout value as int/float (seconds), timedelta, or None.
35
+ Zero values are treated as "disabled" and return None.
36
+
37
+ Returns:
38
+ float seconds if value provided and non-zero, None otherwise
39
+ """
40
+ if value is None:
41
+ return None
42
+ if isinstance(value, datetime.timedelta):
43
+ seconds = value.total_seconds()
44
+ return None if seconds == 0 else seconds
45
+ if isinstance(value, int | float):
46
+ return None if value == 0 else float(value)
47
+ raise TypeError(f"Invalid timeout type: {type(value)}")
@@ -480,7 +480,7 @@ def replace_type(type_, type_map: dict[type, type]):
480
480
  new_args = tuple(replace_type(arg, type_map) for arg in args)
481
481
 
482
482
  if origin is UnionType:
483
- return Union[new_args] # type: ignore # noqa: UP007
483
+ return Union[new_args] # noqa: UP007
484
484
  else:
485
485
  return origin[new_args]
486
486
 
@@ -0,0 +1,285 @@
1
+ """Version comparison utilities for component versioning.
2
+
3
+ This module provides utilities for comparing component versions. Versions are
4
+ strings that are first attempted to be parsed as PEP 440 versions (using the
5
+ `packaging` library), falling back to lexicographic string comparison.
6
+
7
+ Examples:
8
+ - "1", "2", "10" → parsed as PEP 440, compared semantically (1 < 2 < 10)
9
+ - "1.0", "2.0" → parsed as PEP 440
10
+ - "v1.0" → 'v' prefix stripped, parsed as "1.0"
11
+ - "2025-01-15" → not valid PEP 440, compared as strings
12
+ - None → sorts lowest (unversioned components)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from functools import total_ordering
19
+ from typing import TYPE_CHECKING
20
+
21
+ from packaging.version import InvalidVersion, Version
22
+
23
+ if TYPE_CHECKING:
24
+ from fastmcp.utilities.components import FastMCPComponent
25
+
26
+
27
+ @dataclass
28
+ class VersionSpec:
29
+ """Specification for filtering components by version.
30
+
31
+ Used by transforms and providers to filter components to a specific
32
+ version or version range. Unversioned components (version=None) always
33
+ match any spec.
34
+
35
+ Args:
36
+ gte: If set, only versions >= this value match.
37
+ lt: If set, only versions < this value match.
38
+ eq: If set, only this exact version matches (gte/lt ignored).
39
+ """
40
+
41
+ gte: str | None = None
42
+ lt: str | None = None
43
+ eq: str | None = None
44
+
45
+ def matches(self, version: str | None, *, match_none: bool = True) -> bool:
46
+ """Check if a version matches this spec.
47
+
48
+ Args:
49
+ version: The version to check, or None for unversioned.
50
+ match_none: Whether unversioned (None) components match. Defaults to True
51
+ for backward compatibility with retrieval operations. Set to False
52
+ when filtering (e.g., enable/disable) to exclude unversioned components
53
+ from version-specific rules.
54
+
55
+ Returns:
56
+ True if the version matches the spec.
57
+ """
58
+ if version is None:
59
+ return match_none
60
+
61
+ if self.eq is not None:
62
+ return version == self.eq
63
+
64
+ key = parse_version_key(version)
65
+
66
+ if self.gte is not None:
67
+ gte_key = parse_version_key(self.gte)
68
+ if key < gte_key:
69
+ return False
70
+
71
+ if self.lt is not None:
72
+ lt_key = parse_version_key(self.lt)
73
+ if not key < lt_key:
74
+ return False
75
+
76
+ return True
77
+
78
+ def intersect(self, other: VersionSpec | None) -> VersionSpec:
79
+ """Return a spec that satisfies both this spec and other.
80
+
81
+ Used by transforms to combine caller constraints with filter constraints.
82
+ For example, if a VersionFilter has lt="3.0" and caller requests eq="1.0",
83
+ the intersection validates "1.0" is in range and returns the exact spec.
84
+
85
+ Args:
86
+ other: Another spec to intersect with, or None.
87
+
88
+ Returns:
89
+ A VersionSpec that matches only versions satisfying both specs.
90
+ """
91
+ if other is None:
92
+ return self
93
+
94
+ if self.eq is not None:
95
+ # This spec wants exact - validate against other's range
96
+ if other.matches(self.eq):
97
+ return self
98
+ return VersionSpec(eq="__impossible__")
99
+
100
+ if other.eq is not None:
101
+ # Other wants exact - validate against our range
102
+ if self.matches(other.eq):
103
+ return other
104
+ return VersionSpec(eq="__impossible__")
105
+
106
+ # Both are ranges - take tighter bounds
107
+ return VersionSpec(
108
+ gte=max_version(self.gte, other.gte),
109
+ lt=min_version(self.lt, other.lt),
110
+ )
111
+
112
+
113
+ @total_ordering
114
+ class VersionKey:
115
+ """A comparable version key that handles None, PEP 440 versions, and strings.
116
+
117
+ Comparison order:
118
+ 1. None (unversioned) sorts lowest
119
+ 2. PEP 440 versions sort by semantic version order
120
+ 3. Invalid versions (strings) sort lexicographically
121
+ 4. When comparing PEP 440 vs string, PEP 440 comes first
122
+ """
123
+
124
+ __slots__ = ("_is_none", "_is_pep440", "_parsed", "_raw")
125
+
126
+ def __init__(self, version: str | None) -> None:
127
+ self._raw = version
128
+ self._is_none = version is None
129
+ self._is_pep440 = False
130
+ self._parsed: Version | str | None = None
131
+
132
+ if version is not None:
133
+ # Strip leading 'v' if present (common convention like "v1.0")
134
+ normalized = version.lstrip("v") if version.startswith("v") else version
135
+ try:
136
+ self._parsed = Version(normalized)
137
+ self._is_pep440 = True
138
+ except InvalidVersion:
139
+ # Fall back to string comparison for non-PEP 440 versions
140
+ self._parsed = version
141
+
142
+ def __eq__(self, other: object) -> bool:
143
+ if not isinstance(other, VersionKey):
144
+ return NotImplemented
145
+ if self._is_none and other._is_none:
146
+ return True
147
+ if self._is_none != other._is_none:
148
+ return False
149
+ # Both are not None
150
+ if self._is_pep440 and other._is_pep440:
151
+ return self._parsed == other._parsed
152
+ if not self._is_pep440 and not other._is_pep440:
153
+ return self._parsed == other._parsed
154
+ # One is PEP 440, other is string - never equal
155
+ return False
156
+
157
+ def __lt__(self, other: object) -> bool:
158
+ if not isinstance(other, VersionKey):
159
+ return NotImplemented
160
+ # None sorts lowest
161
+ if self._is_none and other._is_none:
162
+ return False # Equal
163
+ if self._is_none:
164
+ return True # None < anything
165
+ if other._is_none:
166
+ return False # anything > None
167
+
168
+ # Both are not None
169
+ if self._is_pep440 and other._is_pep440:
170
+ # Both PEP 440 - compare normally
171
+ assert isinstance(self._parsed, Version)
172
+ assert isinstance(other._parsed, Version)
173
+ return self._parsed < other._parsed
174
+ if not self._is_pep440 and not other._is_pep440:
175
+ # Both strings - lexicographic
176
+ assert isinstance(self._parsed, str)
177
+ assert isinstance(other._parsed, str)
178
+ return self._parsed < other._parsed
179
+ # Mixed: PEP 440 sorts before strings
180
+ # (arbitrary but consistent choice)
181
+ return self._is_pep440
182
+
183
+ def __repr__(self) -> str:
184
+ return f"VersionKey({self._raw!r})"
185
+
186
+
187
+ def parse_version_key(version: str | None) -> VersionKey:
188
+ """Parse a version string into a sortable key.
189
+
190
+ Args:
191
+ version: The version string, or None for unversioned.
192
+
193
+ Returns:
194
+ A VersionKey suitable for sorting.
195
+ """
196
+ return VersionKey(version)
197
+
198
+
199
+ def version_sort_key(component: FastMCPComponent) -> VersionKey:
200
+ """Get a sort key for a component based on its version.
201
+
202
+ Use with sorted() or max() to order components by version.
203
+
204
+ Args:
205
+ component: The component to get a sort key for.
206
+
207
+ Returns:
208
+ A sortable VersionKey.
209
+
210
+ Example:
211
+ ```python
212
+ tools = [tool_v1, tool_v2, tool_unversioned]
213
+ highest = max(tools, key=version_sort_key) # Returns tool_v2
214
+ ```
215
+ """
216
+ return parse_version_key(component.version)
217
+
218
+
219
+ def compare_versions(a: str | None, b: str | None) -> int:
220
+ """Compare two version strings.
221
+
222
+ Args:
223
+ a: First version string (or None).
224
+ b: Second version string (or None).
225
+
226
+ Returns:
227
+ -1 if a < b, 0 if a == b, 1 if a > b.
228
+
229
+ Example:
230
+ ```python
231
+ compare_versions("1.0", "2.0") # Returns -1
232
+ compare_versions("2.0", "1.0") # Returns 1
233
+ compare_versions(None, "1.0") # Returns -1 (None < any version)
234
+ ```
235
+ """
236
+ key_a = parse_version_key(a)
237
+ key_b = parse_version_key(b)
238
+ return (key_a > key_b) - (key_a < key_b)
239
+
240
+
241
+ def is_version_greater(a: str | None, b: str | None) -> bool:
242
+ """Check if version a is greater than version b.
243
+
244
+ Args:
245
+ a: First version string (or None).
246
+ b: Second version string (or None).
247
+
248
+ Returns:
249
+ True if a > b, False otherwise.
250
+ """
251
+ return compare_versions(a, b) > 0
252
+
253
+
254
+ def max_version(a: str | None, b: str | None) -> str | None:
255
+ """Return the greater of two versions.
256
+
257
+ Args:
258
+ a: First version string (or None).
259
+ b: Second version string (or None).
260
+
261
+ Returns:
262
+ The greater version, or None if both are None.
263
+ """
264
+ if a is None:
265
+ return b
266
+ if b is None:
267
+ return a
268
+ return a if compare_versions(a, b) >= 0 else b
269
+
270
+
271
+ def min_version(a: str | None, b: str | None) -> str | None:
272
+ """Return the lesser of two versions.
273
+
274
+ Args:
275
+ a: First version string (or None).
276
+ b: Second version string (or None).
277
+
278
+ Returns:
279
+ The lesser version, or None if both are None.
280
+ """
281
+ if a is None:
282
+ return b
283
+ if b is None:
284
+ return a
285
+ return a if compare_versions(a, b) <= 0 else b
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.14.4
3
+ Version: 3.0.0b1
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
@@ -26,20 +26,23 @@ Requires-Dist: jsonref>=1.1.0
26
26
  Requires-Dist: jsonschema-path>=0.3.4
27
27
  Requires-Dist: mcp<2.0,>=1.24.0
28
28
  Requires-Dist: openapi-pydantic>=0.5.1
29
- Requires-Dist: packaging>=20.0
29
+ Requires-Dist: opentelemetry-api>=1.20.0
30
+ Requires-Dist: packaging>=24.0
30
31
  Requires-Dist: platformdirs>=4.0.0
31
32
  Requires-Dist: py-key-value-aio[disk,keyring,memory]<0.4.0,>=0.3.0
32
33
  Requires-Dist: pydantic[email]>=2.11.7
33
- Requires-Dist: pydocket<0.17.0,>=0.16.6
34
34
  Requires-Dist: pyperclip>=1.9.0
35
35
  Requires-Dist: python-dotenv>=1.1.0
36
36
  Requires-Dist: rich>=13.9.4
37
37
  Requires-Dist: uvicorn>=0.35
38
+ Requires-Dist: watchfiles>=1.0.0
38
39
  Requires-Dist: websockets>=15.0.1
39
40
  Provides-Extra: anthropic
40
41
  Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
41
42
  Provides-Extra: openai
42
43
  Requires-Dist: openai>=1.102.0; extra == 'openai'
44
+ Provides-Extra: tasks
45
+ Requires-Dist: pydocket>=0.16.4; extra == 'tasks'
43
46
  Description-Content-Type: text/markdown
44
47
 
45
48
  <div align="center">
@@ -78,7 +81,7 @@ Description-Content-Type: text/markdown
78
81
  > **For production MCP applications, install FastMCP:** `pip install fastmcp`
79
82
 
80
83
  > [!Important]
81
- > FastMCP 3.0 is in development and may include breaking changes. To avoid unexpected issues, pin your dependency to v2: `fastmcp<3`
84
+ > FastMCP 3.0 is in development and may include some breaking changes. To avoid unexpected issues, pin your dependency to v2: `fastmcp<3`
82
85
 
83
86
  ---
84
87
 
@@ -126,7 +129,7 @@ There are two ways to access the LLM-friendly documentation:
126
129
  ## Table of Contents
127
130
 
128
131
  - [FastMCP v2 🚀](#fastmcp-v2-)
129
- - [📚 Documentation](#-documentation)
132
+ - [📚 Documentation](#-documentation)
130
133
  - [What is MCP?](#what-is-mcp)
131
134
  - [Why FastMCP?](#why-fastmcp)
132
135
  - [Installation](#installation)