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.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {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
|
+
]
|