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,465 @@
|
|
|
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
|
+
LocalProvider can be used standalone and attached to multiple servers:
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from fastmcp.server.providers import LocalProvider
|
|
10
|
+
|
|
11
|
+
# Create a reusable provider with tools
|
|
12
|
+
provider = LocalProvider()
|
|
13
|
+
|
|
14
|
+
@provider.tool
|
|
15
|
+
def greet(name: str) -> str:
|
|
16
|
+
return f"Hello, {name}!"
|
|
17
|
+
|
|
18
|
+
# Attach to any server
|
|
19
|
+
from fastmcp import FastMCP
|
|
20
|
+
server1 = FastMCP("Server1", providers=[provider])
|
|
21
|
+
server2 = FastMCP("Server2", providers=[provider])
|
|
22
|
+
```
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from collections.abc import Sequence
|
|
28
|
+
from typing import Literal, TypeVar
|
|
29
|
+
|
|
30
|
+
from fastmcp.prompts.prompt import Prompt
|
|
31
|
+
from fastmcp.resources.resource import Resource
|
|
32
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
33
|
+
from fastmcp.server.providers.base import Provider
|
|
34
|
+
from fastmcp.server.providers.local_provider.decorators import (
|
|
35
|
+
PromptDecoratorMixin,
|
|
36
|
+
ResourceDecoratorMixin,
|
|
37
|
+
ToolDecoratorMixin,
|
|
38
|
+
)
|
|
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, version_sort_key
|
|
43
|
+
|
|
44
|
+
logger = get_logger(__name__)
|
|
45
|
+
|
|
46
|
+
DuplicateBehavior = Literal["error", "warn", "replace", "ignore"]
|
|
47
|
+
|
|
48
|
+
_C = TypeVar("_C", bound=FastMCPComponent)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LocalProvider(
|
|
52
|
+
Provider,
|
|
53
|
+
ToolDecoratorMixin,
|
|
54
|
+
ResourceDecoratorMixin,
|
|
55
|
+
PromptDecoratorMixin,
|
|
56
|
+
):
|
|
57
|
+
"""Provider for locally-defined components.
|
|
58
|
+
|
|
59
|
+
Supports decorator-based registration (`@provider.tool`, `@provider.resource`,
|
|
60
|
+
`@provider.prompt`) and direct object registration methods.
|
|
61
|
+
|
|
62
|
+
When used standalone, LocalProvider uses default settings. When attached
|
|
63
|
+
to a FastMCP server via the server's decorators, server-level settings
|
|
64
|
+
like `_tool_serializer` and `_support_tasks_by_default` are injected.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
```python
|
|
68
|
+
from fastmcp.server.providers import LocalProvider
|
|
69
|
+
|
|
70
|
+
# Standalone usage
|
|
71
|
+
provider = LocalProvider()
|
|
72
|
+
|
|
73
|
+
@provider.tool
|
|
74
|
+
def greet(name: str) -> str:
|
|
75
|
+
return f"Hello, {name}!"
|
|
76
|
+
|
|
77
|
+
@provider.resource("data://config")
|
|
78
|
+
def get_config() -> str:
|
|
79
|
+
return '{"setting": "value"}'
|
|
80
|
+
|
|
81
|
+
@provider.prompt
|
|
82
|
+
def analyze(topic: str) -> list:
|
|
83
|
+
return [{"role": "user", "content": f"Analyze: {topic}"}]
|
|
84
|
+
|
|
85
|
+
# Attach to server(s)
|
|
86
|
+
from fastmcp import FastMCP
|
|
87
|
+
server = FastMCP("MyServer", providers=[provider])
|
|
88
|
+
```
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
on_duplicate: DuplicateBehavior = "error",
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Initialize a LocalProvider with empty storage.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
on_duplicate: Behavior when adding a component that already exists:
|
|
99
|
+
- "error": Raise ValueError
|
|
100
|
+
- "warn": Log warning and replace
|
|
101
|
+
- "replace": Silently replace
|
|
102
|
+
- "ignore": Keep existing, return it
|
|
103
|
+
"""
|
|
104
|
+
super().__init__()
|
|
105
|
+
self._on_duplicate = on_duplicate
|
|
106
|
+
# Unified component storage - keyed by prefixed key (e.g., "tool:name", "resource:uri")
|
|
107
|
+
self._components: dict[str, FastMCPComponent] = {}
|
|
108
|
+
|
|
109
|
+
# =========================================================================
|
|
110
|
+
# Storage methods
|
|
111
|
+
# =========================================================================
|
|
112
|
+
|
|
113
|
+
def _get_component_identity(self, component: FastMCPComponent) -> tuple[type, str]:
|
|
114
|
+
"""Get the identity (type, name/uri) for a component.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A tuple of (component_type, logical_name) where logical_name is
|
|
118
|
+
the name for tools/prompts or URI for resources/templates.
|
|
119
|
+
"""
|
|
120
|
+
if isinstance(component, Tool):
|
|
121
|
+
return (Tool, component.name)
|
|
122
|
+
elif isinstance(component, ResourceTemplate):
|
|
123
|
+
return (ResourceTemplate, component.uri_template)
|
|
124
|
+
elif isinstance(component, Resource):
|
|
125
|
+
return (Resource, str(component.uri))
|
|
126
|
+
elif isinstance(component, Prompt):
|
|
127
|
+
return (Prompt, component.name)
|
|
128
|
+
else:
|
|
129
|
+
# Fall back to key without version suffix
|
|
130
|
+
key = component.key
|
|
131
|
+
base_key = key.rsplit("@", 1)[0] if "@" in key else key
|
|
132
|
+
return (type(component), base_key)
|
|
133
|
+
|
|
134
|
+
def _check_version_mixing(self, component: _C) -> None:
|
|
135
|
+
"""Check that versioned and unversioned components aren't mixed.
|
|
136
|
+
|
|
137
|
+
LocalProvider enforces a simple rule: for any given name/URI, all
|
|
138
|
+
registered components must either be versioned or unversioned, not both.
|
|
139
|
+
This prevents confusing situations where unversioned components can't
|
|
140
|
+
be filtered out by version filters.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
component: The component being added.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If adding would mix versioned and unversioned components.
|
|
147
|
+
"""
|
|
148
|
+
comp_type, logical_name = self._get_component_identity(component)
|
|
149
|
+
is_versioned = component.version is not None
|
|
150
|
+
|
|
151
|
+
# Check all existing components of the same type and logical name
|
|
152
|
+
for existing in self._components.values():
|
|
153
|
+
if not isinstance(existing, comp_type):
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
_, existing_name = self._get_component_identity(existing)
|
|
157
|
+
if existing_name != logical_name:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
existing_versioned = existing.version is not None
|
|
161
|
+
if is_versioned != existing_versioned:
|
|
162
|
+
type_name = comp_type.__name__.lower()
|
|
163
|
+
if is_versioned:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"Cannot add versioned {type_name} {logical_name!r} "
|
|
166
|
+
f"(version={component.version!r}): an unversioned "
|
|
167
|
+
f"{type_name} with this name already exists. "
|
|
168
|
+
f"Either version all components or none."
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Cannot add unversioned {type_name} {logical_name!r}: "
|
|
173
|
+
f"versioned {type_name}s with this name already exist "
|
|
174
|
+
f"(e.g., version={existing.version!r}). "
|
|
175
|
+
f"Either version all components or none."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _add_component(self, component: _C) -> _C:
|
|
179
|
+
"""Add a component to unified storage.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
component: The component to add.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The component that was added (or existing if on_duplicate="ignore").
|
|
186
|
+
"""
|
|
187
|
+
existing = self._components.get(component.key)
|
|
188
|
+
if existing:
|
|
189
|
+
if self._on_duplicate == "error":
|
|
190
|
+
raise ValueError(f"Component already exists: {component.key}")
|
|
191
|
+
elif self._on_duplicate == "warn":
|
|
192
|
+
logger.warning(f"Component already exists: {component.key}")
|
|
193
|
+
elif self._on_duplicate == "ignore":
|
|
194
|
+
return existing # type: ignore[return-value]
|
|
195
|
+
# "replace" and "warn" fall through to add
|
|
196
|
+
|
|
197
|
+
# Check for versioned/unversioned mixing before adding
|
|
198
|
+
self._check_version_mixing(component)
|
|
199
|
+
|
|
200
|
+
self._components[component.key] = component
|
|
201
|
+
return component
|
|
202
|
+
|
|
203
|
+
def _remove_component(self, key: str) -> None:
|
|
204
|
+
"""Remove a component from unified storage.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
key: The prefixed key of the component.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
KeyError: If the component is not found.
|
|
211
|
+
"""
|
|
212
|
+
component = self._components.get(key)
|
|
213
|
+
if component is None:
|
|
214
|
+
raise KeyError(f"Component {key!r} not found")
|
|
215
|
+
|
|
216
|
+
del self._components[key]
|
|
217
|
+
|
|
218
|
+
def _get_component(self, key: str) -> FastMCPComponent | None:
|
|
219
|
+
"""Get a component by its prefixed key.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
key: The prefixed key (e.g., "tool:name", "resource:uri").
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The component, or None if not found.
|
|
226
|
+
"""
|
|
227
|
+
return self._components.get(key)
|
|
228
|
+
|
|
229
|
+
def remove_tool(self, name: str, version: str | None = None) -> None:
|
|
230
|
+
"""Remove tool(s) from this provider's storage.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
name: The tool name.
|
|
234
|
+
version: If None, removes ALL versions. If specified, removes only that version.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
KeyError: If no matching tool is found.
|
|
238
|
+
"""
|
|
239
|
+
if version is None:
|
|
240
|
+
# Remove all versions
|
|
241
|
+
keys_to_remove = [
|
|
242
|
+
k
|
|
243
|
+
for k, c in self._components.items()
|
|
244
|
+
if isinstance(c, Tool) and c.name == name
|
|
245
|
+
]
|
|
246
|
+
if not keys_to_remove:
|
|
247
|
+
raise KeyError(f"Tool {name!r} not found")
|
|
248
|
+
for key in keys_to_remove:
|
|
249
|
+
self._remove_component(key)
|
|
250
|
+
else:
|
|
251
|
+
# Remove specific version - key format is "tool:name@version"
|
|
252
|
+
key = f"{Tool.make_key(name)}@{version}"
|
|
253
|
+
if key not in self._components:
|
|
254
|
+
raise KeyError(f"Tool {name!r} version {version!r} not found")
|
|
255
|
+
self._remove_component(key)
|
|
256
|
+
|
|
257
|
+
def remove_resource(self, uri: str, version: str | None = None) -> None:
|
|
258
|
+
"""Remove resource(s) from this provider's storage.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
uri: The resource URI.
|
|
262
|
+
version: If None, removes ALL versions. If specified, removes only that version.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
KeyError: If no matching resource is found.
|
|
266
|
+
"""
|
|
267
|
+
if version is None:
|
|
268
|
+
# Remove all versions
|
|
269
|
+
keys_to_remove = [
|
|
270
|
+
k
|
|
271
|
+
for k, c in self._components.items()
|
|
272
|
+
if isinstance(c, Resource) and str(c.uri) == uri
|
|
273
|
+
]
|
|
274
|
+
if not keys_to_remove:
|
|
275
|
+
raise KeyError(f"Resource {uri!r} not found")
|
|
276
|
+
for key in keys_to_remove:
|
|
277
|
+
self._remove_component(key)
|
|
278
|
+
else:
|
|
279
|
+
# Remove specific version
|
|
280
|
+
key = f"{Resource.make_key(uri)}@{version}"
|
|
281
|
+
if key not in self._components:
|
|
282
|
+
raise KeyError(f"Resource {uri!r} version {version!r} not found")
|
|
283
|
+
self._remove_component(key)
|
|
284
|
+
|
|
285
|
+
def remove_template(self, uri_template: str, version: str | None = None) -> None:
|
|
286
|
+
"""Remove resource template(s) from this provider's storage.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
uri_template: The template URI pattern.
|
|
290
|
+
version: If None, removes ALL versions. If specified, removes only that version.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
KeyError: If no matching template is found.
|
|
294
|
+
"""
|
|
295
|
+
if version is None:
|
|
296
|
+
# Remove all versions
|
|
297
|
+
keys_to_remove = [
|
|
298
|
+
k
|
|
299
|
+
for k, c in self._components.items()
|
|
300
|
+
if isinstance(c, ResourceTemplate) and c.uri_template == uri_template
|
|
301
|
+
]
|
|
302
|
+
if not keys_to_remove:
|
|
303
|
+
raise KeyError(f"Template {uri_template!r} not found")
|
|
304
|
+
for key in keys_to_remove:
|
|
305
|
+
self._remove_component(key)
|
|
306
|
+
else:
|
|
307
|
+
# Remove specific version
|
|
308
|
+
key = f"{ResourceTemplate.make_key(uri_template)}@{version}"
|
|
309
|
+
if key not in self._components:
|
|
310
|
+
raise KeyError(
|
|
311
|
+
f"Template {uri_template!r} version {version!r} not found"
|
|
312
|
+
)
|
|
313
|
+
self._remove_component(key)
|
|
314
|
+
|
|
315
|
+
def remove_prompt(self, name: str, version: str | None = None) -> None:
|
|
316
|
+
"""Remove prompt(s) from this provider's storage.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
name: The prompt name.
|
|
320
|
+
version: If None, removes ALL versions. If specified, removes only that version.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
KeyError: If no matching prompt is found.
|
|
324
|
+
"""
|
|
325
|
+
if version is None:
|
|
326
|
+
# Remove all versions
|
|
327
|
+
keys_to_remove = [
|
|
328
|
+
k
|
|
329
|
+
for k, c in self._components.items()
|
|
330
|
+
if isinstance(c, Prompt) and c.name == name
|
|
331
|
+
]
|
|
332
|
+
if not keys_to_remove:
|
|
333
|
+
raise KeyError(f"Prompt {name!r} not found")
|
|
334
|
+
for key in keys_to_remove:
|
|
335
|
+
self._remove_component(key)
|
|
336
|
+
else:
|
|
337
|
+
# Remove specific version
|
|
338
|
+
key = f"{Prompt.make_key(name)}@{version}"
|
|
339
|
+
if key not in self._components:
|
|
340
|
+
raise KeyError(f"Prompt {name!r} version {version!r} not found")
|
|
341
|
+
self._remove_component(key)
|
|
342
|
+
|
|
343
|
+
# =========================================================================
|
|
344
|
+
# Provider interface implementation
|
|
345
|
+
# =========================================================================
|
|
346
|
+
|
|
347
|
+
async def _list_tools(self) -> Sequence[Tool]:
|
|
348
|
+
"""Return all tools."""
|
|
349
|
+
return [v for v in self._components.values() if isinstance(v, Tool)]
|
|
350
|
+
|
|
351
|
+
async def _get_tool(
|
|
352
|
+
self, name: str, version: VersionSpec | None = None
|
|
353
|
+
) -> Tool | None:
|
|
354
|
+
"""Get a tool by name.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
name: The tool name.
|
|
358
|
+
version: Optional version filter. If None, returns highest version.
|
|
359
|
+
"""
|
|
360
|
+
matching = [
|
|
361
|
+
v
|
|
362
|
+
for v in self._components.values()
|
|
363
|
+
if isinstance(v, Tool) and v.name == name
|
|
364
|
+
]
|
|
365
|
+
if version:
|
|
366
|
+
matching = [t for t in matching if version.matches(t.version)]
|
|
367
|
+
if not matching:
|
|
368
|
+
return None
|
|
369
|
+
return max(matching, key=version_sort_key) # type: ignore[type-var]
|
|
370
|
+
|
|
371
|
+
async def _list_resources(self) -> Sequence[Resource]:
|
|
372
|
+
"""Return all resources."""
|
|
373
|
+
return [v for v in self._components.values() if isinstance(v, Resource)]
|
|
374
|
+
|
|
375
|
+
async def _get_resource(
|
|
376
|
+
self, uri: str, version: VersionSpec | None = None
|
|
377
|
+
) -> Resource | None:
|
|
378
|
+
"""Get a resource by URI.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
uri: The resource URI.
|
|
382
|
+
version: Optional version filter. If None, returns highest version.
|
|
383
|
+
"""
|
|
384
|
+
matching = [
|
|
385
|
+
v
|
|
386
|
+
for v in self._components.values()
|
|
387
|
+
if isinstance(v, Resource) and str(v.uri) == uri
|
|
388
|
+
]
|
|
389
|
+
if version:
|
|
390
|
+
matching = [r for r in matching if version.matches(r.version)]
|
|
391
|
+
if not matching:
|
|
392
|
+
return None
|
|
393
|
+
return max(matching, key=version_sort_key) # type: ignore[type-var]
|
|
394
|
+
|
|
395
|
+
async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:
|
|
396
|
+
"""Return all resource templates."""
|
|
397
|
+
return [v for v in self._components.values() if isinstance(v, ResourceTemplate)]
|
|
398
|
+
|
|
399
|
+
async def _get_resource_template(
|
|
400
|
+
self, uri: str, version: VersionSpec | None = None
|
|
401
|
+
) -> ResourceTemplate | None:
|
|
402
|
+
"""Get a resource template that matches the given URI.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
uri: The URI to match against templates.
|
|
406
|
+
version: Optional version filter. If None, returns highest version.
|
|
407
|
+
"""
|
|
408
|
+
# Find all templates that match the URI
|
|
409
|
+
matching = [
|
|
410
|
+
component
|
|
411
|
+
for component in self._components.values()
|
|
412
|
+
if isinstance(component, ResourceTemplate)
|
|
413
|
+
and component.matches(uri) is not None
|
|
414
|
+
]
|
|
415
|
+
if version:
|
|
416
|
+
matching = [t for t in matching if version.matches(t.version)]
|
|
417
|
+
if not matching:
|
|
418
|
+
return None
|
|
419
|
+
return max(matching, key=version_sort_key) # type: ignore[type-var]
|
|
420
|
+
|
|
421
|
+
async def _list_prompts(self) -> Sequence[Prompt]:
|
|
422
|
+
"""Return all prompts."""
|
|
423
|
+
return [v for v in self._components.values() if isinstance(v, Prompt)]
|
|
424
|
+
|
|
425
|
+
async def _get_prompt(
|
|
426
|
+
self, name: str, version: VersionSpec | None = None
|
|
427
|
+
) -> Prompt | None:
|
|
428
|
+
"""Get a prompt by name.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
name: The prompt name.
|
|
432
|
+
version: Optional version filter. If None, returns highest version.
|
|
433
|
+
"""
|
|
434
|
+
matching = [
|
|
435
|
+
v
|
|
436
|
+
for v in self._components.values()
|
|
437
|
+
if isinstance(v, Prompt) and v.name == name
|
|
438
|
+
]
|
|
439
|
+
if version:
|
|
440
|
+
matching = [p for p in matching if version.matches(p.version)]
|
|
441
|
+
if not matching:
|
|
442
|
+
return None
|
|
443
|
+
return max(matching, key=version_sort_key) # type: ignore[type-var]
|
|
444
|
+
|
|
445
|
+
# =========================================================================
|
|
446
|
+
# Task registration
|
|
447
|
+
# =========================================================================
|
|
448
|
+
|
|
449
|
+
async def get_tasks(self) -> Sequence[FastMCPComponent]:
|
|
450
|
+
"""Return components eligible for background task execution.
|
|
451
|
+
|
|
452
|
+
Returns components that have task_config.mode != 'forbidden'.
|
|
453
|
+
This includes both FunctionTool/Resource/Prompt instances created via
|
|
454
|
+
decorators and custom Tool/Resource/Prompt subclasses.
|
|
455
|
+
"""
|
|
456
|
+
return [c for c in self._components.values() if c.task_config.supports_tasks()]
|
|
457
|
+
|
|
458
|
+
# =========================================================================
|
|
459
|
+
# Decorator methods
|
|
460
|
+
# =========================================================================
|
|
461
|
+
# Note: Decorator methods (tool, resource, prompt, add_tool, add_resource,
|
|
462
|
+
# add_template, add_prompt) are provided by mixin classes:
|
|
463
|
+
# - ToolDecoratorMixin
|
|
464
|
+
# - ResourceDecoratorMixin
|
|
465
|
+
# - PromptDecoratorMixin
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""OpenAPI provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides OpenAPI integration for FastMCP through the Provider pattern.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
```python
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
from fastmcp.server.providers.openapi import OpenAPIProvider
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
client = httpx.AsyncClient(base_url="https://api.example.com")
|
|
12
|
+
provider = OpenAPIProvider(openapi_spec=spec, client=client)
|
|
13
|
+
mcp = FastMCP("API Server", providers=[provider])
|
|
14
|
+
```
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from fastmcp.server.providers.openapi.components import (
|
|
18
|
+
OpenAPIResource,
|
|
19
|
+
OpenAPIResourceTemplate,
|
|
20
|
+
OpenAPITool,
|
|
21
|
+
)
|
|
22
|
+
from fastmcp.server.providers.openapi.provider import OpenAPIProvider
|
|
23
|
+
from fastmcp.server.providers.openapi.routing import (
|
|
24
|
+
ComponentFn,
|
|
25
|
+
MCPType,
|
|
26
|
+
RouteMap,
|
|
27
|
+
RouteMapFn,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ComponentFn",
|
|
32
|
+
"MCPType",
|
|
33
|
+
"OpenAPIProvider",
|
|
34
|
+
"OpenAPIResource",
|
|
35
|
+
"OpenAPIResourceTemplate",
|
|
36
|
+
"OpenAPITool",
|
|
37
|
+
"RouteMap",
|
|
38
|
+
"RouteMapFn",
|
|
39
|
+
]
|