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,131 @@
|
|
|
1
|
+
"""Server-side telemetry helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
|
|
6
|
+
from mcp.server.lowlevel.server import request_ctx
|
|
7
|
+
from opentelemetry.context import Context
|
|
8
|
+
from opentelemetry.trace import Span, SpanKind, Status, StatusCode
|
|
9
|
+
|
|
10
|
+
from fastmcp.telemetry import extract_trace_context, get_tracer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_auth_span_attributes() -> dict[str, str]:
|
|
14
|
+
"""Get auth attributes for the current request, if authenticated."""
|
|
15
|
+
from fastmcp.server.dependencies import get_access_token
|
|
16
|
+
|
|
17
|
+
attrs: dict[str, str] = {}
|
|
18
|
+
try:
|
|
19
|
+
token = get_access_token()
|
|
20
|
+
if token:
|
|
21
|
+
if token.client_id:
|
|
22
|
+
attrs["enduser.id"] = token.client_id
|
|
23
|
+
if token.scopes:
|
|
24
|
+
attrs["enduser.scope"] = " ".join(token.scopes)
|
|
25
|
+
except RuntimeError:
|
|
26
|
+
pass
|
|
27
|
+
return attrs
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_session_span_attributes() -> dict[str, str]:
|
|
31
|
+
"""Get session attributes for the current request."""
|
|
32
|
+
from fastmcp.server.dependencies import get_context
|
|
33
|
+
|
|
34
|
+
attrs: dict[str, str] = {}
|
|
35
|
+
try:
|
|
36
|
+
ctx = get_context()
|
|
37
|
+
if ctx.request_context is not None and ctx.session_id is not None:
|
|
38
|
+
attrs["mcp.session.id"] = ctx.session_id
|
|
39
|
+
except RuntimeError:
|
|
40
|
+
pass
|
|
41
|
+
return attrs
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_parent_trace_context() -> Context | None:
|
|
45
|
+
"""Get parent trace context from request meta for distributed tracing."""
|
|
46
|
+
try:
|
|
47
|
+
req_ctx = request_ctx.get()
|
|
48
|
+
if req_ctx and hasattr(req_ctx, "meta") and req_ctx.meta:
|
|
49
|
+
return extract_trace_context(dict(req_ctx.meta))
|
|
50
|
+
except LookupError:
|
|
51
|
+
pass
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@contextmanager
|
|
56
|
+
def server_span(
|
|
57
|
+
name: str,
|
|
58
|
+
method: str,
|
|
59
|
+
server_name: str,
|
|
60
|
+
component_type: str,
|
|
61
|
+
component_key: str,
|
|
62
|
+
resource_uri: str | None = None,
|
|
63
|
+
) -> Generator[Span, None, None]:
|
|
64
|
+
"""Create a SERVER span with standard MCP attributes and auth context.
|
|
65
|
+
|
|
66
|
+
Automatically records any exception on the span and sets error status.
|
|
67
|
+
"""
|
|
68
|
+
tracer = get_tracer()
|
|
69
|
+
with tracer.start_as_current_span(
|
|
70
|
+
name,
|
|
71
|
+
context=_get_parent_trace_context(),
|
|
72
|
+
kind=SpanKind.SERVER,
|
|
73
|
+
) as span:
|
|
74
|
+
attrs: dict[str, str] = {
|
|
75
|
+
# RPC semantic conventions
|
|
76
|
+
"rpc.system": "mcp",
|
|
77
|
+
"rpc.service": server_name,
|
|
78
|
+
"rpc.method": method,
|
|
79
|
+
# MCP semantic conventions
|
|
80
|
+
"mcp.method.name": method,
|
|
81
|
+
# FastMCP-specific attributes
|
|
82
|
+
"fastmcp.server.name": server_name,
|
|
83
|
+
"fastmcp.component.type": component_type,
|
|
84
|
+
"fastmcp.component.key": component_key,
|
|
85
|
+
**get_auth_span_attributes(),
|
|
86
|
+
**get_session_span_attributes(),
|
|
87
|
+
}
|
|
88
|
+
if resource_uri is not None:
|
|
89
|
+
attrs["mcp.resource.uri"] = resource_uri
|
|
90
|
+
span.set_attributes(attrs)
|
|
91
|
+
try:
|
|
92
|
+
yield span
|
|
93
|
+
except Exception as e:
|
|
94
|
+
span.record_exception(e)
|
|
95
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@contextmanager
|
|
100
|
+
def delegate_span(
|
|
101
|
+
name: str,
|
|
102
|
+
provider_type: str,
|
|
103
|
+
component_key: str,
|
|
104
|
+
) -> Generator[Span, None, None]:
|
|
105
|
+
"""Create an INTERNAL span for provider delegation.
|
|
106
|
+
|
|
107
|
+
Used by FastMCPProvider when delegating to mounted servers.
|
|
108
|
+
Automatically records any exception on the span and sets error status.
|
|
109
|
+
"""
|
|
110
|
+
tracer = get_tracer()
|
|
111
|
+
with tracer.start_as_current_span(f"delegate {name}") as span:
|
|
112
|
+
span.set_attributes(
|
|
113
|
+
{
|
|
114
|
+
"fastmcp.provider.type": provider_type,
|
|
115
|
+
"fastmcp.component.key": component_key,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
try:
|
|
119
|
+
yield span
|
|
120
|
+
except Exception as e:
|
|
121
|
+
span.record_exception(e)
|
|
122
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = [
|
|
127
|
+
"delegate_span",
|
|
128
|
+
"get_auth_span_attributes",
|
|
129
|
+
"get_session_span_attributes",
|
|
130
|
+
"server_span",
|
|
131
|
+
]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Transform system for component transformations.
|
|
2
|
+
|
|
3
|
+
Transforms modify components (tools, resources, prompts). List operations use a pure
|
|
4
|
+
function pattern where transforms receive sequences and return transformed sequences.
|
|
5
|
+
Get operations use a middleware pattern with `call_next` to chain lookups.
|
|
6
|
+
|
|
7
|
+
Unlike middleware (which operates on requests), transforms are observable by the
|
|
8
|
+
system for task registration, tag filtering, and component introspection.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
```python
|
|
12
|
+
from fastmcp import FastMCP
|
|
13
|
+
from fastmcp.server.transforms import Namespace
|
|
14
|
+
|
|
15
|
+
server = FastMCP("Server")
|
|
16
|
+
mount = server.mount(other_server)
|
|
17
|
+
mount.add_transform(Namespace("api")) # Tools become api_toolname
|
|
18
|
+
```
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Awaitable, Sequence
|
|
24
|
+
from typing import TYPE_CHECKING, Protocol
|
|
25
|
+
|
|
26
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from fastmcp.prompts.prompt import Prompt
|
|
30
|
+
from fastmcp.resources.resource import Resource
|
|
31
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
32
|
+
from fastmcp.tools.tool import Tool
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Get methods use Protocol to express keyword-only version parameter
|
|
36
|
+
class GetToolNext(Protocol):
|
|
37
|
+
"""Protocol for get_tool call_next functions."""
|
|
38
|
+
|
|
39
|
+
def __call__(
|
|
40
|
+
self, name: str, *, version: VersionSpec | None = None
|
|
41
|
+
) -> Awaitable[Tool | None]: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GetResourceNext(Protocol):
|
|
45
|
+
"""Protocol for get_resource call_next functions."""
|
|
46
|
+
|
|
47
|
+
def __call__(
|
|
48
|
+
self, uri: str, *, version: VersionSpec | None = None
|
|
49
|
+
) -> Awaitable[Resource | None]: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GetResourceTemplateNext(Protocol):
|
|
53
|
+
"""Protocol for get_resource_template call_next functions."""
|
|
54
|
+
|
|
55
|
+
def __call__(
|
|
56
|
+
self, uri: str, *, version: VersionSpec | None = None
|
|
57
|
+
) -> Awaitable[ResourceTemplate | None]: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GetPromptNext(Protocol):
|
|
61
|
+
"""Protocol for get_prompt call_next functions."""
|
|
62
|
+
|
|
63
|
+
def __call__(
|
|
64
|
+
self, name: str, *, version: VersionSpec | None = None
|
|
65
|
+
) -> Awaitable[Prompt | None]: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Transform:
|
|
69
|
+
"""Base class for component transformations.
|
|
70
|
+
|
|
71
|
+
List operations use a pure function pattern: transforms receive sequences
|
|
72
|
+
and return transformed sequences. Get operations use a middleware pattern
|
|
73
|
+
with `call_next` to chain lookups.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
```python
|
|
77
|
+
class MyTransform(Transform):
|
|
78
|
+
async def list_tools(self, tools):
|
|
79
|
+
return [transform(t) for t in tools] # Transform sequence
|
|
80
|
+
|
|
81
|
+
async def get_tool(self, name, call_next, *, version=None):
|
|
82
|
+
original = self.reverse_name(name) # Map to original name
|
|
83
|
+
tool = await call_next(original, version=version) # Get from downstream
|
|
84
|
+
return transform(tool) if tool else None
|
|
85
|
+
```
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __repr__(self) -> str:
|
|
89
|
+
return f"{self.__class__.__name__}()"
|
|
90
|
+
|
|
91
|
+
# -------------------------------------------------------------------------
|
|
92
|
+
# Tools
|
|
93
|
+
# -------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
|
96
|
+
"""List tools with transformation applied.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
tools: Sequence of tools to transform.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Transformed sequence of tools.
|
|
103
|
+
"""
|
|
104
|
+
return tools
|
|
105
|
+
|
|
106
|
+
async def get_tool(
|
|
107
|
+
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
|
|
108
|
+
) -> Tool | None:
|
|
109
|
+
"""Get a tool by name.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
name: The requested tool name (may be transformed).
|
|
113
|
+
call_next: Callable to get tool from downstream.
|
|
114
|
+
version: Optional version filter to apply.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The tool if found, None otherwise.
|
|
118
|
+
"""
|
|
119
|
+
return await call_next(name, version=version)
|
|
120
|
+
|
|
121
|
+
# -------------------------------------------------------------------------
|
|
122
|
+
# Resources
|
|
123
|
+
# -------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
|
|
126
|
+
"""List resources with transformation applied.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
resources: Sequence of resources to transform.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Transformed sequence of resources.
|
|
133
|
+
"""
|
|
134
|
+
return resources
|
|
135
|
+
|
|
136
|
+
async def get_resource(
|
|
137
|
+
self,
|
|
138
|
+
uri: str,
|
|
139
|
+
call_next: GetResourceNext,
|
|
140
|
+
*,
|
|
141
|
+
version: VersionSpec | None = None,
|
|
142
|
+
) -> Resource | None:
|
|
143
|
+
"""Get a resource by URI.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
uri: The requested resource URI (may be transformed).
|
|
147
|
+
call_next: Callable to get resource from downstream.
|
|
148
|
+
version: Optional version filter to apply.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The resource if found, None otherwise.
|
|
152
|
+
"""
|
|
153
|
+
return await call_next(uri, version=version)
|
|
154
|
+
|
|
155
|
+
# -------------------------------------------------------------------------
|
|
156
|
+
# Resource Templates
|
|
157
|
+
# -------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
async def list_resource_templates(
|
|
160
|
+
self, templates: Sequence[ResourceTemplate]
|
|
161
|
+
) -> Sequence[ResourceTemplate]:
|
|
162
|
+
"""List resource templates with transformation applied.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
templates: Sequence of resource templates to transform.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Transformed sequence of resource templates.
|
|
169
|
+
"""
|
|
170
|
+
return templates
|
|
171
|
+
|
|
172
|
+
async def get_resource_template(
|
|
173
|
+
self,
|
|
174
|
+
uri: str,
|
|
175
|
+
call_next: GetResourceTemplateNext,
|
|
176
|
+
*,
|
|
177
|
+
version: VersionSpec | None = None,
|
|
178
|
+
) -> ResourceTemplate | None:
|
|
179
|
+
"""Get a resource template by URI.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
uri: The requested template URI (may be transformed).
|
|
183
|
+
call_next: Callable to get template from downstream.
|
|
184
|
+
version: Optional version filter to apply.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The resource template if found, None otherwise.
|
|
188
|
+
"""
|
|
189
|
+
return await call_next(uri, version=version)
|
|
190
|
+
|
|
191
|
+
# -------------------------------------------------------------------------
|
|
192
|
+
# Prompts
|
|
193
|
+
# -------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
|
|
196
|
+
"""List prompts with transformation applied.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
prompts: Sequence of prompts to transform.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Transformed sequence of prompts.
|
|
203
|
+
"""
|
|
204
|
+
return prompts
|
|
205
|
+
|
|
206
|
+
async def get_prompt(
|
|
207
|
+
self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None
|
|
208
|
+
) -> Prompt | None:
|
|
209
|
+
"""Get a prompt by name.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
name: The requested prompt name (may be transformed).
|
|
213
|
+
call_next: Callable to get prompt from downstream.
|
|
214
|
+
version: Optional version filter to apply.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
The prompt if found, None otherwise.
|
|
218
|
+
"""
|
|
219
|
+
return await call_next(name, version=version)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Re-export built-in transforms (must be after Transform class to avoid circular imports)
|
|
223
|
+
from fastmcp.server.transforms.visibility import Visibility, is_enabled # noqa: E402
|
|
224
|
+
from fastmcp.server.transforms.namespace import Namespace # noqa: E402
|
|
225
|
+
from fastmcp.server.transforms.prompts_as_tools import PromptsAsTools # noqa: E402
|
|
226
|
+
from fastmcp.server.transforms.resources_as_tools import ResourcesAsTools # noqa: E402
|
|
227
|
+
from fastmcp.server.transforms.tool_transform import ToolTransform # noqa: E402
|
|
228
|
+
from fastmcp.server.transforms.version_filter import VersionFilter # noqa: E402
|
|
229
|
+
|
|
230
|
+
__all__ = [
|
|
231
|
+
"GetPromptNext",
|
|
232
|
+
"GetResourceNext",
|
|
233
|
+
"GetResourceTemplateNext",
|
|
234
|
+
"GetToolNext",
|
|
235
|
+
"Namespace",
|
|
236
|
+
"PromptsAsTools",
|
|
237
|
+
"ResourcesAsTools",
|
|
238
|
+
"ToolTransform",
|
|
239
|
+
"Transform",
|
|
240
|
+
"VersionFilter",
|
|
241
|
+
"VersionSpec",
|
|
242
|
+
"Visibility",
|
|
243
|
+
"is_enabled",
|
|
244
|
+
]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Namespace transform for prefixing component names."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from fastmcp.server.transforms import (
|
|
10
|
+
GetPromptNext,
|
|
11
|
+
GetResourceNext,
|
|
12
|
+
GetResourceTemplateNext,
|
|
13
|
+
GetToolNext,
|
|
14
|
+
Transform,
|
|
15
|
+
)
|
|
16
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from fastmcp.prompts.prompt import Prompt
|
|
20
|
+
from fastmcp.resources.resource import Resource
|
|
21
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
22
|
+
from fastmcp.tools.tool import Tool
|
|
23
|
+
|
|
24
|
+
# Pattern for matching URIs: protocol://path
|
|
25
|
+
_URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Namespace(Transform):
|
|
29
|
+
"""Prefixes component names with a namespace.
|
|
30
|
+
|
|
31
|
+
- Tools: name → namespace_name
|
|
32
|
+
- Prompts: name → namespace_name
|
|
33
|
+
- Resources: protocol://path → protocol://namespace/path
|
|
34
|
+
- Resource Templates: same as resources
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
```python
|
|
38
|
+
transform = Namespace("math")
|
|
39
|
+
# Tool "add" becomes "math_add"
|
|
40
|
+
# Resource "file://data.txt" becomes "file://math/data.txt"
|
|
41
|
+
```
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, prefix: str) -> None:
|
|
45
|
+
"""Initialize Namespace transform.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
prefix: The namespace prefix to apply.
|
|
49
|
+
"""
|
|
50
|
+
self._prefix = prefix
|
|
51
|
+
self._name_prefix = f"{prefix}_"
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return f"Namespace({self._prefix!r})"
|
|
55
|
+
|
|
56
|
+
# -------------------------------------------------------------------------
|
|
57
|
+
# Name transformation helpers
|
|
58
|
+
# -------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def _transform_name(self, name: str) -> str:
|
|
61
|
+
"""Apply namespace prefix to a name."""
|
|
62
|
+
return f"{self._name_prefix}{name}"
|
|
63
|
+
|
|
64
|
+
def _reverse_name(self, name: str) -> str | None:
|
|
65
|
+
"""Remove namespace prefix from a name, or None if no match."""
|
|
66
|
+
if name.startswith(self._name_prefix):
|
|
67
|
+
return name[len(self._name_prefix) :]
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# -------------------------------------------------------------------------
|
|
71
|
+
# URI transformation helpers
|
|
72
|
+
# -------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def _transform_uri(self, uri: str) -> str:
|
|
75
|
+
"""Apply namespace to a URI: protocol://path → protocol://namespace/path."""
|
|
76
|
+
match = _URI_PATTERN.match(uri)
|
|
77
|
+
if match:
|
|
78
|
+
protocol, path = match.groups()
|
|
79
|
+
return f"{protocol}{self._prefix}/{path}"
|
|
80
|
+
return uri
|
|
81
|
+
|
|
82
|
+
def _reverse_uri(self, uri: str) -> str | None:
|
|
83
|
+
"""Remove namespace from a URI, or None if no match."""
|
|
84
|
+
match = _URI_PATTERN.match(uri)
|
|
85
|
+
if match:
|
|
86
|
+
protocol, path = match.groups()
|
|
87
|
+
prefix = f"{self._prefix}/"
|
|
88
|
+
if path.startswith(prefix):
|
|
89
|
+
return f"{protocol}{path[len(prefix) :]}"
|
|
90
|
+
return None
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# -------------------------------------------------------------------------
|
|
94
|
+
# Tools
|
|
95
|
+
# -------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
|
98
|
+
"""Prefix tool names with namespace."""
|
|
99
|
+
return [
|
|
100
|
+
t.model_copy(update={"name": self._transform_name(t.name)}) for t in tools
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
async def get_tool(
|
|
104
|
+
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
|
|
105
|
+
) -> Tool | None:
|
|
106
|
+
"""Get tool by namespaced name."""
|
|
107
|
+
original = self._reverse_name(name)
|
|
108
|
+
if original is None:
|
|
109
|
+
return None
|
|
110
|
+
tool = await call_next(original, version=version)
|
|
111
|
+
if tool:
|
|
112
|
+
return tool.model_copy(update={"name": name})
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
# -------------------------------------------------------------------------
|
|
116
|
+
# Resources
|
|
117
|
+
# -------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
|
|
120
|
+
"""Add namespace path segment to resource URIs."""
|
|
121
|
+
return [
|
|
122
|
+
r.model_copy(update={"uri": self._transform_uri(str(r.uri))})
|
|
123
|
+
for r in resources
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
async def get_resource(
|
|
127
|
+
self,
|
|
128
|
+
uri: str,
|
|
129
|
+
call_next: GetResourceNext,
|
|
130
|
+
*,
|
|
131
|
+
version: VersionSpec | None = None,
|
|
132
|
+
) -> Resource | None:
|
|
133
|
+
"""Get resource by namespaced URI."""
|
|
134
|
+
original = self._reverse_uri(uri)
|
|
135
|
+
if original is None:
|
|
136
|
+
return None
|
|
137
|
+
resource = await call_next(original, version=version)
|
|
138
|
+
if resource:
|
|
139
|
+
return resource.model_copy(update={"uri": uri})
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# -------------------------------------------------------------------------
|
|
143
|
+
# Resource Templates
|
|
144
|
+
# -------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
async def list_resource_templates(
|
|
147
|
+
self, templates: Sequence[ResourceTemplate]
|
|
148
|
+
) -> Sequence[ResourceTemplate]:
|
|
149
|
+
"""Add namespace path segment to template URIs."""
|
|
150
|
+
return [
|
|
151
|
+
t.model_copy(update={"uri_template": self._transform_uri(t.uri_template)})
|
|
152
|
+
for t in templates
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
async def get_resource_template(
|
|
156
|
+
self,
|
|
157
|
+
uri: str,
|
|
158
|
+
call_next: GetResourceTemplateNext,
|
|
159
|
+
*,
|
|
160
|
+
version: VersionSpec | None = None,
|
|
161
|
+
) -> ResourceTemplate | None:
|
|
162
|
+
"""Get resource template by namespaced URI."""
|
|
163
|
+
original = self._reverse_uri(uri)
|
|
164
|
+
if original is None:
|
|
165
|
+
return None
|
|
166
|
+
template = await call_next(original, version=version)
|
|
167
|
+
if template:
|
|
168
|
+
return template.model_copy(
|
|
169
|
+
update={"uri_template": self._transform_uri(template.uri_template)}
|
|
170
|
+
)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
# -------------------------------------------------------------------------
|
|
174
|
+
# Prompts
|
|
175
|
+
# -------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
|
|
178
|
+
"""Prefix prompt names with namespace."""
|
|
179
|
+
return [
|
|
180
|
+
p.model_copy(update={"name": self._transform_name(p.name)}) for p in prompts
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
async def get_prompt(
|
|
184
|
+
self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None
|
|
185
|
+
) -> Prompt | None:
|
|
186
|
+
"""Get prompt by namespaced name."""
|
|
187
|
+
original = self._reverse_name(name)
|
|
188
|
+
if original is None:
|
|
189
|
+
return None
|
|
190
|
+
prompt = await call_next(original, version=version)
|
|
191
|
+
if prompt:
|
|
192
|
+
return prompt.model_copy(update={"name": name})
|
|
193
|
+
return None
|