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,226 @@
1
+ """FileSystemProvider for filesystem-based component discovery.
2
+
3
+ FileSystemProvider scans a directory for Python files, imports them, and
4
+ registers any Tool, Resource, ResourceTemplate, or Prompt objects found.
5
+
6
+ Components are created using the standalone decorators from fastmcp.tools,
7
+ fastmcp.resources, and fastmcp.prompts:
8
+
9
+ Example:
10
+ ```python
11
+ # In mcp/tools.py
12
+ from fastmcp.tools import tool
13
+
14
+ @tool
15
+ def greet(name: str) -> str:
16
+ return f"Hello, {name}!"
17
+
18
+ # In main.py
19
+ from pathlib import Path
20
+
21
+ from fastmcp import FastMCP
22
+ from fastmcp.server.providers import FileSystemProvider
23
+
24
+ mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "mcp")])
25
+ ```
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import asyncio
31
+ from collections.abc import Sequence
32
+ from pathlib import Path
33
+
34
+ from fastmcp.prompts.prompt import Prompt
35
+ from fastmcp.resources.resource import Resource
36
+ from fastmcp.resources.template import ResourceTemplate
37
+ from fastmcp.server.providers.filesystem_discovery import discover_and_import
38
+ from fastmcp.server.providers.local_provider import LocalProvider
39
+ from fastmcp.tools.tool import Tool
40
+ from fastmcp.utilities.components import FastMCPComponent
41
+ from fastmcp.utilities.logging import get_logger
42
+ from fastmcp.utilities.versions import VersionSpec
43
+
44
+ logger = get_logger(__name__)
45
+
46
+
47
+ class FileSystemProvider(LocalProvider):
48
+ """Provider that discovers components from the filesystem.
49
+
50
+ Scans a directory for Python files and registers any Tool, Resource,
51
+ ResourceTemplate, or Prompt objects found. Components are created using
52
+ the standalone decorators:
53
+ - @tool from fastmcp.tools
54
+ - @resource from fastmcp.resources
55
+ - @prompt from fastmcp.prompts
56
+
57
+ Args:
58
+ root: Root directory to scan. Defaults to current directory.
59
+ reload: If True, re-scan files on every request (dev mode).
60
+ Defaults to False (scan once at init, cache results).
61
+
62
+ Example:
63
+ ```python
64
+ # In mcp/tools.py
65
+ from fastmcp.tools import tool
66
+
67
+ @tool
68
+ def greet(name: str) -> str:
69
+ return f"Hello, {name}!"
70
+
71
+ # In main.py
72
+ from pathlib import Path
73
+
74
+ from fastmcp import FastMCP
75
+ from fastmcp.server.providers import FileSystemProvider
76
+
77
+ # Path relative to this file
78
+ mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "mcp")])
79
+
80
+ # Dev mode - re-scan on every request
81
+ mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "mcp", reload=True)])
82
+ ```
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ root: str | Path = ".",
88
+ reload: bool = False,
89
+ ) -> None:
90
+ super().__init__(on_duplicate="replace")
91
+ self._root = Path(root).resolve()
92
+ self._reload = reload
93
+ self._loaded = False
94
+ # Track files we've warned about: path -> mtime when warned
95
+ # Re-warn if file changes (mtime differs)
96
+ self._warned_files: dict[Path, float] = {}
97
+ # Lock for serializing reload operations (created lazily)
98
+ self._reload_lock: asyncio.Lock | None = None
99
+
100
+ # Always load once at init to catch errors early
101
+ self._load_components()
102
+
103
+ def _load_components(self) -> None:
104
+ """Discover and register all components from the filesystem."""
105
+ # Clear existing components if reloading
106
+ if self._loaded:
107
+ self._components.clear()
108
+
109
+ result = discover_and_import(self._root)
110
+
111
+ # Log warnings for failed files (only once per file version)
112
+ for file_path, error in result.failed_files.items():
113
+ try:
114
+ current_mtime = file_path.stat().st_mtime
115
+ except OSError:
116
+ current_mtime = 0.0
117
+
118
+ # Warn if we haven't warned about this file, or if it changed
119
+ last_warned_mtime = self._warned_files.get(file_path)
120
+ if last_warned_mtime is None or last_warned_mtime != current_mtime:
121
+ logger.warning(f"Failed to import {file_path}: {error}")
122
+ self._warned_files[file_path] = current_mtime
123
+
124
+ # Clear warnings for files that now import successfully
125
+ successful_files = {fp for fp, _ in result.components}
126
+ for fp in successful_files:
127
+ self._warned_files.pop(fp, None)
128
+
129
+ for file_path, component in result.components:
130
+ try:
131
+ self._register_component(component)
132
+ except Exception:
133
+ logger.exception(
134
+ "Failed to register %s from %s",
135
+ getattr(component, "name", repr(component)),
136
+ file_path,
137
+ )
138
+
139
+ self._loaded = True
140
+ logger.debug(
141
+ f"FileSystemProvider loaded {len(self._components)} components from {self._root}"
142
+ )
143
+
144
+ def _register_component(self, component: FastMCPComponent) -> None:
145
+ """Register a single component based on its type."""
146
+ if isinstance(component, Tool):
147
+ self.add_tool(component)
148
+ elif isinstance(component, ResourceTemplate):
149
+ self.add_template(component)
150
+ elif isinstance(component, Resource):
151
+ self.add_resource(component)
152
+ elif isinstance(component, Prompt):
153
+ self.add_prompt(component)
154
+ else:
155
+ logger.debug("Ignoring unknown component type: %r", type(component))
156
+
157
+ async def _ensure_loaded(self) -> None:
158
+ """Ensure components are loaded, reloading if in reload mode.
159
+
160
+ Uses a lock to serialize concurrent reload operations and runs
161
+ filesystem I/O off the event loop using asyncio.to_thread.
162
+ """
163
+ if not self._reload and self._loaded:
164
+ return
165
+
166
+ # Create lock lazily (can't create in __init__ without event loop)
167
+ if self._reload_lock is None:
168
+ self._reload_lock = asyncio.Lock()
169
+
170
+ async with self._reload_lock:
171
+ # Double-check after acquiring lock
172
+ if self._reload or not self._loaded:
173
+ await asyncio.to_thread(self._load_components)
174
+
175
+ # Override provider methods to support reload mode
176
+
177
+ async def _list_tools(self) -> Sequence[Tool]:
178
+ """Return all tools, reloading if in reload mode."""
179
+ await self._ensure_loaded()
180
+ return await super()._list_tools()
181
+
182
+ async def _get_tool(
183
+ self, name: str, version: VersionSpec | None = None
184
+ ) -> Tool | None:
185
+ """Get a tool by name, reloading if in reload mode."""
186
+ await self._ensure_loaded()
187
+ return await super()._get_tool(name, version)
188
+
189
+ async def _list_resources(self) -> Sequence[Resource]:
190
+ """Return all resources, reloading if in reload mode."""
191
+ await self._ensure_loaded()
192
+ return await super()._list_resources()
193
+
194
+ async def _get_resource(
195
+ self, uri: str, version: VersionSpec | None = None
196
+ ) -> Resource | None:
197
+ """Get a resource by URI, reloading if in reload mode."""
198
+ await self._ensure_loaded()
199
+ return await super()._get_resource(uri, version)
200
+
201
+ async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:
202
+ """Return all resource templates, reloading if in reload mode."""
203
+ await self._ensure_loaded()
204
+ return await super()._list_resource_templates()
205
+
206
+ async def _get_resource_template(
207
+ self, uri: str, version: VersionSpec | None = None
208
+ ) -> ResourceTemplate | None:
209
+ """Get a resource template, reloading if in reload mode."""
210
+ await self._ensure_loaded()
211
+ return await super()._get_resource_template(uri, version)
212
+
213
+ async def _list_prompts(self) -> Sequence[Prompt]:
214
+ """Return all prompts, reloading if in reload mode."""
215
+ await self._ensure_loaded()
216
+ return await super()._list_prompts()
217
+
218
+ async def _get_prompt(
219
+ self, name: str, version: VersionSpec | None = None
220
+ ) -> Prompt | None:
221
+ """Get a prompt by name, reloading if in reload mode."""
222
+ await self._ensure_loaded()
223
+ return await super()._get_prompt(name, version)
224
+
225
+ def __repr__(self) -> str:
226
+ return f"FileSystemProvider(root={self._root!r}, reload={self._reload})"
@@ -0,0 +1,327 @@
1
+ """File discovery and module import utilities for filesystem-based routing.
2
+
3
+ This module provides functions to:
4
+ 1. Discover Python files in a directory tree
5
+ 2. Import modules (as packages if __init__.py exists, else directly)
6
+ 3. Extract decorated components (Tool, Resource, Prompt objects) from imported modules
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ import sys
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+ from types import ModuleType
16
+
17
+ from fastmcp.utilities.components import FastMCPComponent
18
+ from fastmcp.utilities.logging import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ @dataclass
24
+ class DiscoveryResult:
25
+ """Result of filesystem discovery."""
26
+
27
+ # Components are real objects (Tool, Resource, ResourceTemplate, Prompt)
28
+ components: list[tuple[Path, FastMCPComponent]] = field(default_factory=list)
29
+ failed_files: dict[Path, str] = field(default_factory=dict) # path -> error message
30
+
31
+
32
+ def discover_files(root: Path) -> list[Path]:
33
+ """Recursively discover all Python files under a directory.
34
+
35
+ Excludes __init__.py files (they're for package structure, not components).
36
+
37
+ Args:
38
+ root: Root directory to scan.
39
+
40
+ Returns:
41
+ List of .py file paths, sorted for deterministic order.
42
+ """
43
+ if not root.exists():
44
+ return []
45
+
46
+ if not root.is_dir():
47
+ # If root is a file, just return it (if it's a .py file)
48
+ if root.suffix == ".py" and root.name != "__init__.py":
49
+ return [root]
50
+ return []
51
+
52
+ files: list[Path] = []
53
+ for path in root.rglob("*.py"):
54
+ # Skip __init__.py files
55
+ if path.name == "__init__.py":
56
+ continue
57
+ # Skip __pycache__ directories
58
+ if "__pycache__" in path.parts:
59
+ continue
60
+ files.append(path)
61
+
62
+ # Sort for deterministic discovery order
63
+ return sorted(files)
64
+
65
+
66
+ def _is_package_dir(directory: Path) -> bool:
67
+ """Check if a directory is a Python package (has __init__.py)."""
68
+ return (directory / "__init__.py").exists()
69
+
70
+
71
+ def _find_package_root(file_path: Path) -> Path | None:
72
+ """Find the root of the package containing this file.
73
+
74
+ Walks up the directory tree until we find a directory without __init__.py.
75
+
76
+ Returns:
77
+ The package root directory, or None if not in a package.
78
+ """
79
+ current = file_path.parent
80
+ package_root = None
81
+
82
+ while current != current.parent: # Stop at filesystem root
83
+ if _is_package_dir(current):
84
+ package_root = current
85
+ current = current.parent
86
+ else:
87
+ break
88
+
89
+ return package_root
90
+
91
+
92
+ def _compute_module_name(file_path: Path, package_root: Path) -> str:
93
+ """Compute the dotted module name for a file within a package.
94
+
95
+ Args:
96
+ file_path: Path to the Python file.
97
+ package_root: Root directory of the package.
98
+
99
+ Returns:
100
+ Dotted module name (e.g., "mcp.tools.greet").
101
+ """
102
+ relative = file_path.relative_to(package_root.parent)
103
+ parts = list(relative.parts)
104
+ # Remove .py extension from last part
105
+ parts[-1] = parts[-1].removesuffix(".py")
106
+ return ".".join(parts)
107
+
108
+
109
+ def import_module_from_file(file_path: Path) -> ModuleType:
110
+ """Import a Python file as a module.
111
+
112
+ If the file is part of a package (directory has __init__.py), imports
113
+ it as a proper package member (relative imports work). Otherwise,
114
+ imports directly using spec_from_file_location.
115
+
116
+ Args:
117
+ file_path: Path to the Python file.
118
+
119
+ Returns:
120
+ The imported module.
121
+
122
+ Raises:
123
+ ImportError: If the module cannot be imported.
124
+ """
125
+ file_path = file_path.resolve()
126
+
127
+ # Check if this file is part of a package
128
+ package_root = _find_package_root(file_path)
129
+
130
+ if package_root is not None:
131
+ # Import as part of a package
132
+ module_name = _compute_module_name(file_path, package_root)
133
+
134
+ # Ensure package root's parent is in sys.path
135
+ package_parent = str(package_root.parent)
136
+ if package_parent not in sys.path:
137
+ sys.path.insert(0, package_parent)
138
+
139
+ # Import using standard import machinery
140
+ # If already imported, reload to pick up changes (for reload mode)
141
+ try:
142
+ if module_name in sys.modules:
143
+ return importlib.reload(sys.modules[module_name])
144
+ return importlib.import_module(module_name)
145
+ except ImportError as e:
146
+ raise ImportError(
147
+ f"Failed to import {module_name} from {file_path}: {e}"
148
+ ) from e
149
+ else:
150
+ # Import directly using spec_from_file_location
151
+ module_name = file_path.stem
152
+
153
+ # Ensure parent directory is in sys.path for imports
154
+ parent_dir = str(file_path.parent)
155
+ if parent_dir not in sys.path:
156
+ sys.path.insert(0, parent_dir)
157
+
158
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
159
+ if spec is None or spec.loader is None:
160
+ raise ImportError(f"Cannot load spec for {file_path}")
161
+
162
+ module = importlib.util.module_from_spec(spec)
163
+ sys.modules[module_name] = module
164
+
165
+ try:
166
+ spec.loader.exec_module(module)
167
+ except Exception as e:
168
+ # Clean up sys.modules on failure
169
+ sys.modules.pop(module_name, None)
170
+ raise ImportError(f"Failed to execute module {file_path}: {e}") from e
171
+
172
+ return module
173
+
174
+
175
+ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
176
+ """Extract all MCP components from a module.
177
+
178
+ Scans all module attributes for instances of Tool, Resource,
179
+ ResourceTemplate, or Prompt objects created by standalone decorators,
180
+ or functions decorated with @tool/@resource/@prompt that have __fastmcp__ metadata.
181
+
182
+ Args:
183
+ module: The imported module to scan.
184
+
185
+ Returns:
186
+ List of component objects (Tool, Resource, ResourceTemplate, Prompt).
187
+ """
188
+ # Import here to avoid circular imports
189
+ import inspect
190
+
191
+ from fastmcp.decorators import get_fastmcp_meta
192
+ from fastmcp.prompts.function_prompt import PromptMeta
193
+ from fastmcp.prompts.prompt import Prompt
194
+ from fastmcp.resources.function_resource import ResourceMeta
195
+ from fastmcp.resources.resource import Resource
196
+ from fastmcp.resources.template import ResourceTemplate
197
+ from fastmcp.server.dependencies import without_injected_parameters
198
+ from fastmcp.tools.function_tool import ToolMeta
199
+ from fastmcp.tools.tool import Tool
200
+
201
+ component_types = (Tool, Resource, ResourceTemplate, Prompt)
202
+ components: list[FastMCPComponent] = []
203
+
204
+ for name in dir(module):
205
+ # Skip private/magic attributes
206
+ if name.startswith("_"):
207
+ continue
208
+
209
+ try:
210
+ obj = getattr(module, name)
211
+ except AttributeError:
212
+ continue
213
+
214
+ # Check if this object is a component type
215
+ if isinstance(obj, component_types):
216
+ components.append(obj)
217
+ continue
218
+
219
+ # Check for functions with __fastmcp__ metadata
220
+ meta = get_fastmcp_meta(obj)
221
+ if meta is not None:
222
+ if isinstance(meta, ToolMeta):
223
+ resolved_task = meta.task if meta.task is not None else False
224
+ tool = Tool.from_function(
225
+ obj,
226
+ name=meta.name,
227
+ title=meta.title,
228
+ description=meta.description,
229
+ icons=meta.icons,
230
+ tags=meta.tags,
231
+ output_schema=meta.output_schema,
232
+ annotations=meta.annotations,
233
+ meta=meta.meta,
234
+ task=resolved_task,
235
+ exclude_args=meta.exclude_args,
236
+ serializer=meta.serializer,
237
+ auth=meta.auth,
238
+ )
239
+ components.append(tool)
240
+ elif isinstance(meta, ResourceMeta):
241
+ resolved_task = meta.task if meta.task is not None else False
242
+ has_uri_params = "{" in meta.uri and "}" in meta.uri
243
+ wrapper_fn = without_injected_parameters(obj)
244
+ has_func_params = bool(inspect.signature(wrapper_fn).parameters)
245
+
246
+ if has_uri_params or has_func_params:
247
+ resource = ResourceTemplate.from_function(
248
+ fn=obj,
249
+ uri_template=meta.uri,
250
+ name=meta.name,
251
+ title=meta.title,
252
+ description=meta.description,
253
+ icons=meta.icons,
254
+ mime_type=meta.mime_type,
255
+ tags=meta.tags,
256
+ annotations=meta.annotations,
257
+ meta=meta.meta,
258
+ task=resolved_task,
259
+ auth=meta.auth,
260
+ )
261
+ else:
262
+ resource = Resource.from_function(
263
+ fn=obj,
264
+ uri=meta.uri,
265
+ name=meta.name,
266
+ title=meta.title,
267
+ description=meta.description,
268
+ icons=meta.icons,
269
+ mime_type=meta.mime_type,
270
+ tags=meta.tags,
271
+ annotations=meta.annotations,
272
+ meta=meta.meta,
273
+ task=resolved_task,
274
+ auth=meta.auth,
275
+ )
276
+ components.append(resource)
277
+ elif isinstance(meta, PromptMeta):
278
+ resolved_task = meta.task if meta.task is not None else False
279
+ prompt = Prompt.from_function(
280
+ obj,
281
+ name=meta.name,
282
+ title=meta.title,
283
+ description=meta.description,
284
+ icons=meta.icons,
285
+ tags=meta.tags,
286
+ meta=meta.meta,
287
+ task=resolved_task,
288
+ auth=meta.auth,
289
+ )
290
+ components.append(prompt)
291
+
292
+ return components
293
+
294
+
295
+ def discover_and_import(root: Path) -> DiscoveryResult:
296
+ """Discover files, import modules, and extract components.
297
+
298
+ This is the main entry point for filesystem-based discovery.
299
+
300
+ Args:
301
+ root: Root directory to scan.
302
+
303
+ Returns:
304
+ DiscoveryResult with components and any failed files.
305
+
306
+ Note:
307
+ Files that fail to import are tracked in failed_files, not logged.
308
+ The caller is responsible for logging/handling failures.
309
+ Files with no components are silently skipped.
310
+ """
311
+ result = DiscoveryResult()
312
+
313
+ for file_path in discover_files(root):
314
+ try:
315
+ module = import_module_from_file(file_path)
316
+ except ImportError as e:
317
+ result.failed_files[file_path] = str(e)
318
+ continue
319
+ except Exception as e:
320
+ result.failed_files[file_path] = str(e)
321
+ continue
322
+
323
+ components = extract_components(module)
324
+ for component in components:
325
+ result.components.append((file_path, component))
326
+
327
+ return result
@@ -0,0 +1,11 @@
1
+ """LocalProvider for locally-defined MCP components.
2
+
3
+ This module provides the `LocalProvider` class that manages tools, resources,
4
+ templates, and prompts registered via decorators or direct methods.
5
+ """
6
+
7
+ from fastmcp.server.providers.local_provider.local_provider import (
8
+ LocalProvider,
9
+ )
10
+
11
+ __all__ = ["LocalProvider"]
@@ -0,0 +1,15 @@
1
+ """Decorator mixins for LocalProvider.
2
+
3
+ This module provides mixin classes that add decorator functionality
4
+ to LocalProvider for tools, resources, templates, and prompts.
5
+ """
6
+
7
+ from .prompts import PromptDecoratorMixin
8
+ from .resources import ResourceDecoratorMixin
9
+ from .tools import ToolDecoratorMixin
10
+
11
+ __all__ = [
12
+ "PromptDecoratorMixin",
13
+ "ResourceDecoratorMixin",
14
+ "ToolDecoratorMixin",
15
+ ]