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,867 @@
1
+ """ProxyProvider for proxying to remote MCP servers.
2
+
3
+ This module provides the `ProxyProvider` class that proxies components from
4
+ a remote MCP server via a client factory. It also provides proxy component
5
+ classes that forward execution to remote servers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import inspect
12
+ from collections.abc import Awaitable, Callable, Sequence
13
+ from typing import TYPE_CHECKING, Any, cast
14
+ from urllib.parse import quote
15
+
16
+ import mcp.types
17
+ from mcp import ServerSession
18
+ from mcp.client.session import ClientSession
19
+ from mcp.shared.context import LifespanContextT, RequestContext
20
+ from mcp.shared.exceptions import McpError
21
+ from mcp.types import (
22
+ METHOD_NOT_FOUND,
23
+ BlobResourceContents,
24
+ ElicitRequestFormParams,
25
+ TextResourceContents,
26
+ )
27
+ from pydantic.networks import AnyUrl
28
+
29
+ from fastmcp.client.client import Client, FastMCP1Server
30
+ from fastmcp.client.elicitation import ElicitResult
31
+ from fastmcp.client.logging import LogMessage
32
+ from fastmcp.client.roots import RootsList
33
+ from fastmcp.client.telemetry import client_span
34
+ from fastmcp.client.transports import ClientTransportT
35
+ from fastmcp.exceptions import ResourceError, ToolError
36
+ from fastmcp.mcp_config import MCPConfig
37
+ from fastmcp.prompts import Message, Prompt, PromptResult
38
+ from fastmcp.prompts.prompt import PromptArgument
39
+ from fastmcp.resources import Resource, ResourceTemplate
40
+ from fastmcp.resources.resource import ResourceContent, ResourceResult
41
+ from fastmcp.server.context import Context
42
+ from fastmcp.server.dependencies import get_context
43
+ from fastmcp.server.providers.base import Provider
44
+ from fastmcp.server.server import FastMCP
45
+ from fastmcp.server.tasks.config import TaskConfig
46
+ from fastmcp.tools.tool import Tool, ToolResult
47
+ from fastmcp.utilities.components import FastMCPComponent, get_fastmcp_metadata
48
+ from fastmcp.utilities.logging import get_logger
49
+
50
+ if TYPE_CHECKING:
51
+ from pathlib import Path
52
+
53
+ from fastmcp.client.transports import ClientTransport
54
+
55
+ logger = get_logger(__name__)
56
+
57
+ # Type alias for client factory functions
58
+ ClientFactoryT = Callable[[], Client] | Callable[[], Awaitable[Client]]
59
+
60
+
61
+ # -----------------------------------------------------------------------------
62
+ # Proxy Component Classes
63
+ # -----------------------------------------------------------------------------
64
+
65
+
66
+ class ProxyTool(Tool):
67
+ """A Tool that represents and executes a tool on a remote server."""
68
+
69
+ task_config: TaskConfig = TaskConfig(mode="forbidden")
70
+ _backend_name: str | None = None
71
+
72
+ def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
73
+ super().__init__(**kwargs)
74
+ self._client_factory = client_factory
75
+
76
+ async def _get_client(self) -> Client:
77
+ """Gets a client instance by calling the sync or async factory."""
78
+ client = self._client_factory()
79
+ if inspect.isawaitable(client):
80
+ client = await client
81
+ return client
82
+
83
+ def model_copy(self, **kwargs: Any) -> ProxyTool:
84
+ """Override to preserve _backend_name when name changes."""
85
+ update = kwargs.get("update", {})
86
+ if "name" in update and self._backend_name is None:
87
+ # First time name is being changed, preserve original for backend calls
88
+ update = {**update, "_backend_name": self.name}
89
+ kwargs["update"] = update
90
+ return super().model_copy(**kwargs)
91
+
92
+ @classmethod
93
+ def from_mcp_tool(
94
+ cls, client_factory: ClientFactoryT, mcp_tool: mcp.types.Tool
95
+ ) -> ProxyTool:
96
+ """Factory method to create a ProxyTool from a raw MCP tool schema."""
97
+ return cls(
98
+ client_factory=client_factory,
99
+ name=mcp_tool.name,
100
+ title=mcp_tool.title,
101
+ description=mcp_tool.description,
102
+ parameters=mcp_tool.inputSchema,
103
+ annotations=mcp_tool.annotations,
104
+ output_schema=mcp_tool.outputSchema,
105
+ icons=mcp_tool.icons,
106
+ meta=mcp_tool.meta,
107
+ tags=get_fastmcp_metadata(mcp_tool.meta).get("tags", []),
108
+ )
109
+
110
+ async def run(
111
+ self,
112
+ arguments: dict[str, Any],
113
+ context: Context | None = None,
114
+ ) -> ToolResult:
115
+ """Executes the tool by making a call through the client."""
116
+ backend_name = self._backend_name or self.name
117
+ with client_span(
118
+ f"tools/call {backend_name}", "tools/call", backend_name
119
+ ) as span:
120
+ span.set_attribute("fastmcp.provider.type", "ProxyProvider")
121
+ client = await self._get_client()
122
+ async with client:
123
+ ctx = context or get_context()
124
+ # Build meta dict from request context
125
+ meta: dict[str, Any] | None = None
126
+ if hasattr(ctx, "request_context"):
127
+ req_ctx = ctx.request_context
128
+ # Start with existing meta if present
129
+ if hasattr(req_ctx, "meta") and req_ctx.meta:
130
+ meta = dict(req_ctx.meta)
131
+ # Add task metadata if this is a task request
132
+ if (
133
+ hasattr(req_ctx, "experimental")
134
+ and hasattr(req_ctx.experimental, "is_task")
135
+ and req_ctx.experimental.is_task
136
+ ):
137
+ task_metadata = req_ctx.experimental.task_metadata
138
+ if task_metadata:
139
+ meta = meta or {}
140
+ meta["modelcontextprotocol.io/task"] = (
141
+ task_metadata.model_dump(exclude_none=True)
142
+ )
143
+
144
+ result = await client.call_tool_mcp(
145
+ name=backend_name, arguments=arguments, meta=meta
146
+ )
147
+ if result.isError:
148
+ raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
149
+ # Preserve backend's meta (includes task metadata for background tasks)
150
+ return ToolResult(
151
+ content=result.content,
152
+ structured_content=result.structuredContent,
153
+ meta=result.meta,
154
+ )
155
+
156
+ def get_span_attributes(self) -> dict[str, Any]:
157
+ return super().get_span_attributes() | {
158
+ "fastmcp.provider.type": "ProxyProvider",
159
+ "fastmcp.proxy.backend_name": self._backend_name,
160
+ }
161
+
162
+
163
+ class ProxyResource(Resource):
164
+ """A Resource that represents and reads a resource from a remote server."""
165
+
166
+ task_config: TaskConfig = TaskConfig(mode="forbidden")
167
+ _cached_content: ResourceResult | None = None
168
+ _backend_uri: str | None = None
169
+
170
+ def __init__(
171
+ self,
172
+ client_factory: ClientFactoryT,
173
+ *,
174
+ _cached_content: ResourceResult | None = None,
175
+ **kwargs,
176
+ ):
177
+ super().__init__(**kwargs)
178
+ self._client_factory = client_factory
179
+ self._cached_content = _cached_content
180
+
181
+ async def _get_client(self) -> Client:
182
+ """Gets a client instance by calling the sync or async factory."""
183
+ client = self._client_factory()
184
+ if inspect.isawaitable(client):
185
+ client = await client
186
+ return client
187
+
188
+ def model_copy(self, **kwargs: Any) -> ProxyResource:
189
+ """Override to preserve _backend_uri when uri changes."""
190
+ update = kwargs.get("update", {})
191
+ if "uri" in update and self._backend_uri is None:
192
+ # First time uri is being changed, preserve original for backend calls
193
+ update = {**update, "_backend_uri": str(self.uri)}
194
+ kwargs["update"] = update
195
+ return super().model_copy(**kwargs)
196
+
197
+ @classmethod
198
+ def from_mcp_resource(
199
+ cls,
200
+ client_factory: ClientFactoryT,
201
+ mcp_resource: mcp.types.Resource,
202
+ ) -> ProxyResource:
203
+ """Factory method to create a ProxyResource from a raw MCP resource schema."""
204
+
205
+ return cls(
206
+ client_factory=client_factory,
207
+ uri=mcp_resource.uri,
208
+ name=mcp_resource.name,
209
+ title=mcp_resource.title,
210
+ description=mcp_resource.description,
211
+ mime_type=mcp_resource.mimeType or "text/plain",
212
+ icons=mcp_resource.icons,
213
+ meta=mcp_resource.meta,
214
+ tags=get_fastmcp_metadata(mcp_resource.meta).get("tags", []),
215
+ task_config=TaskConfig(mode="forbidden"),
216
+ )
217
+
218
+ async def read(self) -> ResourceResult:
219
+ """Read the resource content from the remote server."""
220
+ if self._cached_content is not None:
221
+ return self._cached_content
222
+
223
+ backend_uri = self._backend_uri or str(self.uri)
224
+ with client_span(
225
+ f"resources/read {backend_uri}",
226
+ "resources/read",
227
+ backend_uri,
228
+ resource_uri=backend_uri,
229
+ ) as span:
230
+ span.set_attribute("fastmcp.provider.type", "ProxyProvider")
231
+ client = await self._get_client()
232
+ async with client:
233
+ result = await client.read_resource(backend_uri)
234
+ if not result:
235
+ raise ResourceError(
236
+ f"Remote server returned empty content for {backend_uri}"
237
+ )
238
+
239
+ # Process all items in the result list, not just the first one
240
+ contents: list[ResourceContent] = []
241
+ for item in result:
242
+ if isinstance(item, TextResourceContents):
243
+ contents.append(
244
+ ResourceContent(
245
+ content=item.text,
246
+ mime_type=item.mimeType,
247
+ meta=item.meta,
248
+ )
249
+ )
250
+ elif isinstance(item, BlobResourceContents):
251
+ contents.append(
252
+ ResourceContent(
253
+ content=base64.b64decode(item.blob),
254
+ mime_type=item.mimeType,
255
+ meta=item.meta,
256
+ )
257
+ )
258
+ else:
259
+ raise ResourceError(f"Unsupported content type: {type(item)}")
260
+
261
+ return ResourceResult(contents=contents)
262
+
263
+ def get_span_attributes(self) -> dict[str, Any]:
264
+ return super().get_span_attributes() | {
265
+ "fastmcp.provider.type": "ProxyProvider",
266
+ "fastmcp.proxy.backend_uri": self._backend_uri,
267
+ }
268
+
269
+
270
+ class ProxyTemplate(ResourceTemplate):
271
+ """A ResourceTemplate that represents and creates resources from a remote server template."""
272
+
273
+ task_config: TaskConfig = TaskConfig(mode="forbidden")
274
+ _backend_uri_template: str | None = None
275
+
276
+ def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
277
+ super().__init__(**kwargs)
278
+ self._client_factory = client_factory
279
+
280
+ async def _get_client(self) -> Client:
281
+ """Gets a client instance by calling the sync or async factory."""
282
+ client = self._client_factory()
283
+ if inspect.isawaitable(client):
284
+ client = await client
285
+ return client
286
+
287
+ def model_copy(self, **kwargs: Any) -> ProxyTemplate:
288
+ """Override to preserve _backend_uri_template when uri_template changes."""
289
+ update = kwargs.get("update", {})
290
+ if "uri_template" in update and self._backend_uri_template is None:
291
+ # First time uri_template is being changed, preserve original for backend
292
+ update = {**update, "_backend_uri_template": self.uri_template}
293
+ kwargs["update"] = update
294
+ return super().model_copy(**kwargs)
295
+
296
+ @classmethod
297
+ def from_mcp_template( # type: ignore[override]
298
+ cls, client_factory: ClientFactoryT, mcp_template: mcp.types.ResourceTemplate
299
+ ) -> ProxyTemplate:
300
+ """Factory method to create a ProxyTemplate from a raw MCP template schema."""
301
+
302
+ return cls(
303
+ client_factory=client_factory,
304
+ uri_template=mcp_template.uriTemplate,
305
+ name=mcp_template.name,
306
+ title=mcp_template.title,
307
+ description=mcp_template.description,
308
+ mime_type=mcp_template.mimeType or "text/plain",
309
+ icons=mcp_template.icons,
310
+ parameters={}, # Remote templates don't have local parameters
311
+ meta=mcp_template.meta,
312
+ tags=get_fastmcp_metadata(mcp_template.meta).get("tags", []),
313
+ task_config=TaskConfig(mode="forbidden"),
314
+ )
315
+
316
+ async def create_resource(
317
+ self,
318
+ uri: str,
319
+ params: dict[str, Any],
320
+ context: Context | None = None,
321
+ ) -> ProxyResource:
322
+ """Create a resource from the template by calling the remote server."""
323
+ # don't use the provided uri, because it may not be the same as the
324
+ # uri_template on the remote server.
325
+ # quote params to ensure they are valid for the uri_template
326
+ backend_template = self._backend_uri_template or self.uri_template
327
+ parameterized_uri = backend_template.format(
328
+ **{k: quote(v, safe="") for k, v in params.items()}
329
+ )
330
+ client = await self._get_client()
331
+ async with client:
332
+ result = await client.read_resource(parameterized_uri)
333
+
334
+ if not result:
335
+ raise ResourceError(
336
+ f"Remote server returned empty content for {parameterized_uri}"
337
+ )
338
+
339
+ # Process all items in the result list, not just the first one
340
+ contents: list[ResourceContent] = []
341
+ for item in result:
342
+ if isinstance(item, TextResourceContents):
343
+ contents.append(
344
+ ResourceContent(
345
+ content=item.text,
346
+ mime_type=item.mimeType,
347
+ meta=item.meta,
348
+ )
349
+ )
350
+ elif isinstance(item, BlobResourceContents):
351
+ contents.append(
352
+ ResourceContent(
353
+ content=base64.b64decode(item.blob),
354
+ mime_type=item.mimeType,
355
+ meta=item.meta,
356
+ )
357
+ )
358
+ else:
359
+ raise ResourceError(f"Unsupported content type: {type(item)}")
360
+
361
+ cached_content = ResourceResult(contents=contents)
362
+
363
+ return ProxyResource(
364
+ client_factory=self._client_factory,
365
+ uri=parameterized_uri,
366
+ name=self.name,
367
+ title=self.title,
368
+ description=self.description,
369
+ mime_type=result[
370
+ 0
371
+ ].mimeType, # Use first item's mimeType for backward compatibility
372
+ icons=self.icons,
373
+ meta=self.meta,
374
+ tags=get_fastmcp_metadata(self.meta).get("tags", []),
375
+ _cached_content=cached_content,
376
+ )
377
+
378
+ def get_span_attributes(self) -> dict[str, Any]:
379
+ return super().get_span_attributes() | {
380
+ "fastmcp.provider.type": "ProxyProvider",
381
+ "fastmcp.proxy.backend_uri_template": self._backend_uri_template,
382
+ }
383
+
384
+
385
+ class ProxyPrompt(Prompt):
386
+ """A Prompt that represents and renders a prompt from a remote server."""
387
+
388
+ task_config: TaskConfig = TaskConfig(mode="forbidden")
389
+ _backend_name: str | None = None
390
+
391
+ def __init__(self, client_factory: ClientFactoryT, **kwargs):
392
+ super().__init__(**kwargs)
393
+ self._client_factory = client_factory
394
+
395
+ async def _get_client(self) -> Client:
396
+ """Gets a client instance by calling the sync or async factory."""
397
+ client = self._client_factory()
398
+ if inspect.isawaitable(client):
399
+ client = await client
400
+ return client
401
+
402
+ def model_copy(self, **kwargs: Any) -> ProxyPrompt:
403
+ """Override to preserve _backend_name when name changes."""
404
+ update = kwargs.get("update", {})
405
+ if "name" in update and self._backend_name is None:
406
+ # First time name is being changed, preserve original for backend calls
407
+ update = {**update, "_backend_name": self.name}
408
+ kwargs["update"] = update
409
+ return super().model_copy(**kwargs)
410
+
411
+ @classmethod
412
+ def from_mcp_prompt(
413
+ cls, client_factory: ClientFactoryT, mcp_prompt: mcp.types.Prompt
414
+ ) -> ProxyPrompt:
415
+ """Factory method to create a ProxyPrompt from a raw MCP prompt schema."""
416
+ arguments = [
417
+ PromptArgument(
418
+ name=arg.name,
419
+ description=arg.description,
420
+ required=arg.required or False,
421
+ )
422
+ for arg in mcp_prompt.arguments or []
423
+ ]
424
+ return cls(
425
+ client_factory=client_factory,
426
+ name=mcp_prompt.name,
427
+ title=mcp_prompt.title,
428
+ description=mcp_prompt.description,
429
+ arguments=arguments,
430
+ icons=mcp_prompt.icons,
431
+ meta=mcp_prompt.meta,
432
+ tags=get_fastmcp_metadata(mcp_prompt.meta).get("tags", []),
433
+ task_config=TaskConfig(mode="forbidden"),
434
+ )
435
+
436
+ async def render(self, arguments: dict[str, Any]) -> PromptResult: # type: ignore[override]
437
+ """Render the prompt by making a call through the client."""
438
+ backend_name = self._backend_name or self.name
439
+ with client_span(
440
+ f"prompts/get {backend_name}", "prompts/get", backend_name
441
+ ) as span:
442
+ span.set_attribute("fastmcp.provider.type", "ProxyProvider")
443
+ client = await self._get_client()
444
+ async with client:
445
+ result = await client.get_prompt(backend_name, arguments)
446
+ # Convert GetPromptResult to PromptResult, preserving meta from result
447
+ # (not the static prompt meta which includes fastmcp tags)
448
+ # Convert PromptMessages to Messages
449
+ messages = [
450
+ Message(content=m.content, role=m.role) for m in result.messages
451
+ ]
452
+ return PromptResult(
453
+ messages=messages,
454
+ description=result.description,
455
+ meta=result.meta,
456
+ )
457
+
458
+ def get_span_attributes(self) -> dict[str, Any]:
459
+ return super().get_span_attributes() | {
460
+ "fastmcp.provider.type": "ProxyProvider",
461
+ "fastmcp.proxy.backend_name": self._backend_name,
462
+ }
463
+
464
+
465
+ # -----------------------------------------------------------------------------
466
+ # ProxyProvider
467
+ # -----------------------------------------------------------------------------
468
+
469
+
470
+ class ProxyProvider(Provider):
471
+ """Provider that proxies to a remote MCP server via a client factory.
472
+
473
+ This provider fetches components from a remote server and returns Proxy*
474
+ component instances that forward execution to the remote server.
475
+
476
+ All components returned by this provider have task_config.mode="forbidden"
477
+ because tasks cannot be executed through a proxy.
478
+
479
+ Example:
480
+ ```python
481
+ from fastmcp import FastMCP
482
+ from fastmcp.server.providers.proxy import ProxyProvider, ProxyClient
483
+
484
+ # Create a proxy provider for a remote server
485
+ proxy = ProxyProvider(lambda: ProxyClient("http://localhost:8000/mcp"))
486
+
487
+ mcp = FastMCP("Proxy Server")
488
+ mcp.add_provider(proxy)
489
+
490
+ # Can also add with namespace
491
+ mcp.add_provider(proxy.with_namespace("remote"))
492
+ ```
493
+ """
494
+
495
+ def __init__(
496
+ self,
497
+ client_factory: ClientFactoryT,
498
+ ):
499
+ """Initialize a ProxyProvider.
500
+
501
+ Args:
502
+ client_factory: A callable that returns a Client instance when called.
503
+ This gives you full control over session creation and reuse.
504
+ Can be either a synchronous or asynchronous function.
505
+ """
506
+ super().__init__()
507
+ self.client_factory = client_factory
508
+
509
+ async def _get_client(self) -> Client:
510
+ """Gets a client instance by calling the sync or async factory."""
511
+ client = self.client_factory()
512
+ if inspect.isawaitable(client):
513
+ client = await client
514
+ return client
515
+
516
+ # -------------------------------------------------------------------------
517
+ # Tool methods
518
+ # -------------------------------------------------------------------------
519
+
520
+ async def _list_tools(self) -> Sequence[Tool]:
521
+ """List all tools from the remote server."""
522
+ try:
523
+ client = await self._get_client()
524
+ async with client:
525
+ mcp_tools = await client.list_tools()
526
+ return [
527
+ ProxyTool.from_mcp_tool(self.client_factory, t) for t in mcp_tools
528
+ ]
529
+ except McpError as e:
530
+ if e.error.code == METHOD_NOT_FOUND:
531
+ return []
532
+ raise
533
+
534
+ # -------------------------------------------------------------------------
535
+ # Resource methods
536
+ # -------------------------------------------------------------------------
537
+
538
+ async def _list_resources(self) -> Sequence[Resource]:
539
+ """List all resources from the remote server."""
540
+ try:
541
+ client = await self._get_client()
542
+ async with client:
543
+ mcp_resources = await client.list_resources()
544
+ return [
545
+ ProxyResource.from_mcp_resource(self.client_factory, r)
546
+ for r in mcp_resources
547
+ ]
548
+ except McpError as e:
549
+ if e.error.code == METHOD_NOT_FOUND:
550
+ return []
551
+ raise
552
+
553
+ # -------------------------------------------------------------------------
554
+ # Resource template methods
555
+ # -------------------------------------------------------------------------
556
+
557
+ async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:
558
+ """List all resource templates from the remote server."""
559
+ try:
560
+ client = await self._get_client()
561
+ async with client:
562
+ mcp_templates = await client.list_resource_templates()
563
+ return [
564
+ ProxyTemplate.from_mcp_template(self.client_factory, t)
565
+ for t in mcp_templates
566
+ ]
567
+ except McpError as e:
568
+ if e.error.code == METHOD_NOT_FOUND:
569
+ return []
570
+ raise
571
+
572
+ # -------------------------------------------------------------------------
573
+ # Prompt methods
574
+ # -------------------------------------------------------------------------
575
+
576
+ async def _list_prompts(self) -> Sequence[Prompt]:
577
+ """List all prompts from the remote server."""
578
+ try:
579
+ client = await self._get_client()
580
+ async with client:
581
+ mcp_prompts = await client.list_prompts()
582
+ return [
583
+ ProxyPrompt.from_mcp_prompt(self.client_factory, p)
584
+ for p in mcp_prompts
585
+ ]
586
+ except McpError as e:
587
+ if e.error.code == METHOD_NOT_FOUND:
588
+ return []
589
+ raise
590
+
591
+ # -------------------------------------------------------------------------
592
+ # Task methods
593
+ # -------------------------------------------------------------------------
594
+
595
+ async def get_tasks(self) -> Sequence[FastMCPComponent]:
596
+ """Return empty list since proxy components don't support tasks.
597
+
598
+ Override the base implementation to avoid calling list_tools() during
599
+ server lifespan initialization, which would open the client before any
600
+ context is set. All Proxy* components have task_config.mode="forbidden".
601
+ """
602
+ return []
603
+
604
+ # lifespan() uses default implementation (empty context manager)
605
+ # because client cleanup is handled per-request
606
+
607
+
608
+ # -----------------------------------------------------------------------------
609
+ # Factory Functions
610
+ # -----------------------------------------------------------------------------
611
+
612
+
613
+ def _create_client_factory(
614
+ target: (
615
+ Client[ClientTransportT]
616
+ | ClientTransport
617
+ | FastMCP[Any]
618
+ | FastMCP1Server
619
+ | AnyUrl
620
+ | Path
621
+ | MCPConfig
622
+ | dict[str, Any]
623
+ | str
624
+ ),
625
+ ) -> ClientFactoryT:
626
+ """Create a client factory from the given target.
627
+
628
+ Internal helper that handles the session strategy based on the target type:
629
+ - Connected Client: reuses existing session (with warning about context mixing)
630
+ - Disconnected Client: creates fresh sessions per request
631
+ - Other targets: creates ProxyClient and fresh sessions per request
632
+ """
633
+ if isinstance(target, Client):
634
+ client = target
635
+ if client.is_connected():
636
+ logger.info(
637
+ "Proxy detected connected client - reusing existing session for all requests. "
638
+ "This may cause context mixing in concurrent scenarios."
639
+ )
640
+
641
+ def reuse_client_factory() -> Client:
642
+ return client
643
+
644
+ return reuse_client_factory
645
+ else:
646
+
647
+ def fresh_client_factory() -> Client:
648
+ return client.new()
649
+
650
+ return fresh_client_factory
651
+ else:
652
+ # target is not a Client, so it's compatible with ProxyClient.__init__
653
+ base_client = ProxyClient(cast(Any, target))
654
+
655
+ def proxy_client_factory() -> Client:
656
+ return base_client.new()
657
+
658
+ return proxy_client_factory
659
+
660
+
661
+ # -----------------------------------------------------------------------------
662
+ # FastMCPProxy - Convenience Wrapper
663
+ # -----------------------------------------------------------------------------
664
+
665
+
666
+ class FastMCPProxy(FastMCP):
667
+ """A FastMCP server that acts as a proxy to a remote MCP-compliant server.
668
+
669
+ This is a convenience wrapper that creates a FastMCP server with a
670
+ ProxyProvider. For more control, use FastMCP with add_provider(ProxyProvider(...)).
671
+
672
+ Example:
673
+ ```python
674
+ from fastmcp.server import create_proxy
675
+ from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient
676
+
677
+ # Create a proxy server using create_proxy (recommended)
678
+ proxy = create_proxy("http://localhost:8000/mcp")
679
+
680
+ # Or use FastMCPProxy directly with explicit client factory
681
+ proxy = FastMCPProxy(client_factory=lambda: ProxyClient("http://localhost:8000/mcp"))
682
+ ```
683
+ """
684
+
685
+ def __init__(
686
+ self,
687
+ *,
688
+ client_factory: ClientFactoryT,
689
+ **kwargs,
690
+ ):
691
+ """Initialize the proxy server.
692
+
693
+ FastMCPProxy requires explicit session management via client_factory.
694
+ Use create_proxy() for convenience with automatic session strategy.
695
+
696
+ Args:
697
+ client_factory: A callable that returns a Client instance when called.
698
+ This gives you full control over session creation and reuse.
699
+ Can be either a synchronous or asynchronous function.
700
+ **kwargs: Additional settings for the FastMCP server.
701
+ """
702
+ super().__init__(**kwargs)
703
+ self.client_factory = client_factory
704
+ provider: Provider = ProxyProvider(client_factory)
705
+ self.add_provider(provider)
706
+
707
+
708
+ # -----------------------------------------------------------------------------
709
+ # ProxyClient and Related
710
+ # -----------------------------------------------------------------------------
711
+
712
+
713
+ async def default_proxy_roots_handler(
714
+ context: RequestContext[ClientSession, LifespanContextT],
715
+ ) -> RootsList:
716
+ """Forward list roots request from remote server to proxy's connected clients."""
717
+ ctx = get_context()
718
+ return await ctx.list_roots()
719
+
720
+
721
+ async def default_proxy_sampling_handler(
722
+ messages: list[mcp.types.SamplingMessage],
723
+ params: mcp.types.CreateMessageRequestParams,
724
+ context: RequestContext[ClientSession, LifespanContextT],
725
+ ) -> mcp.types.CreateMessageResult:
726
+ """Forward sampling request from remote server to proxy's connected clients."""
727
+ ctx = get_context()
728
+ result = await ctx.sample(
729
+ list(messages),
730
+ system_prompt=params.systemPrompt,
731
+ temperature=params.temperature,
732
+ max_tokens=params.maxTokens,
733
+ model_preferences=params.modelPreferences,
734
+ )
735
+ content = mcp.types.TextContent(type="text", text=result.text or "")
736
+ return mcp.types.CreateMessageResult(
737
+ role="assistant",
738
+ model="fastmcp-client",
739
+ # TODO(ty): remove when ty supports isinstance exclusion narrowing
740
+ content=content,
741
+ )
742
+
743
+
744
+ async def default_proxy_elicitation_handler(
745
+ message: str,
746
+ response_type: type,
747
+ params: mcp.types.ElicitRequestParams,
748
+ context: RequestContext[ClientSession, LifespanContextT],
749
+ ) -> ElicitResult:
750
+ """Forward elicitation request from remote server to proxy's connected clients."""
751
+ ctx = get_context()
752
+ # requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams
753
+ requested_schema = (
754
+ params.requestedSchema
755
+ if isinstance(params, ElicitRequestFormParams)
756
+ else {"type": "object", "properties": {}}
757
+ )
758
+ result = await ctx.session.elicit(
759
+ message=message,
760
+ requestedSchema=requested_schema,
761
+ related_request_id=ctx.request_id,
762
+ )
763
+ return ElicitResult(action=result.action, content=result.content)
764
+
765
+
766
+ async def default_proxy_log_handler(message: LogMessage) -> None:
767
+ """Forward log notification from remote server to proxy's connected clients."""
768
+ ctx = get_context()
769
+ msg = message.data.get("msg")
770
+ extra = message.data.get("extra")
771
+ await ctx.log(msg, level=message.level, logger_name=message.logger, extra=extra)
772
+
773
+
774
+ async def default_proxy_progress_handler(
775
+ progress: float,
776
+ total: float | None,
777
+ message: str | None,
778
+ ) -> None:
779
+ """Forward progress notification from remote server to proxy's connected clients."""
780
+ ctx = get_context()
781
+ await ctx.report_progress(progress, total, message)
782
+
783
+
784
+ class ProxyClient(Client[ClientTransportT]):
785
+ """A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
786
+
787
+ Supports forwarding roots, sampling, elicitation, logging, and progress.
788
+ """
789
+
790
+ # Stored context for handlers when contextvar isn't available
791
+ # (e.g., when receive loop was started before any request context)
792
+ _proxy_context: Context | None = None
793
+
794
+ def __init__(
795
+ self,
796
+ transport: ClientTransportT
797
+ | FastMCP[Any]
798
+ | FastMCP1Server
799
+ | AnyUrl
800
+ | Path
801
+ | MCPConfig
802
+ | dict[str, Any]
803
+ | str,
804
+ **kwargs,
805
+ ):
806
+ if "name" not in kwargs:
807
+ kwargs["name"] = self.generate_name()
808
+ if "roots" not in kwargs:
809
+ kwargs["roots"] = default_proxy_roots_handler
810
+ if "sampling_handler" not in kwargs:
811
+ kwargs["sampling_handler"] = default_proxy_sampling_handler
812
+ if "elicitation_handler" not in kwargs:
813
+ kwargs["elicitation_handler"] = default_proxy_elicitation_handler
814
+ if "log_handler" not in kwargs:
815
+ kwargs["log_handler"] = default_proxy_log_handler
816
+ if "progress_handler" not in kwargs:
817
+ kwargs["progress_handler"] = default_proxy_progress_handler
818
+ super().__init__(**kwargs | {"transport": transport})
819
+
820
+
821
+ class StatefulProxyClient(ProxyClient[ClientTransportT]):
822
+ """A proxy client that provides a stateful client factory for the proxy server.
823
+
824
+ The stateful proxy client bound its copy to the server session.
825
+ And it will be disconnected when the session is exited.
826
+
827
+ This is useful to proxy a stateful mcp server such as the Playwright MCP server.
828
+ Note that it is essential to ensure that the proxy server itself is also stateful.
829
+ """
830
+
831
+ def __init__(self, *args: Any, **kwargs: Any):
832
+ super().__init__(*args, **kwargs)
833
+ self._caches: dict[ServerSession, Client[ClientTransportT]] = {}
834
+
835
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[override]
836
+ """The stateful proxy client will be forced disconnected when the session is exited.
837
+
838
+ So we do nothing here.
839
+ """
840
+
841
+ async def clear(self):
842
+ """Clear all cached clients and force disconnect them."""
843
+ while self._caches:
844
+ _, cache = self._caches.popitem()
845
+ await cache._disconnect(force=True)
846
+
847
+ def new_stateful(self) -> Client[ClientTransportT]:
848
+ """Create a new stateful proxy client instance with the same configuration.
849
+
850
+ Use this method as the client factory for stateful proxy server.
851
+ """
852
+ session = get_context().session
853
+ proxy_client = self._caches.get(session, None)
854
+
855
+ if proxy_client is None:
856
+ proxy_client = self.new()
857
+ logger.debug(f"{proxy_client} created for {session}")
858
+ self._caches[session] = proxy_client
859
+
860
+ async def _on_session_exit():
861
+ self._caches.pop(session)
862
+ logger.debug(f"{proxy_client} will be disconnect")
863
+ await proxy_client._disconnect(force=True)
864
+
865
+ session._exit_stack.push_async_callback(_on_session_exit)
866
+
867
+ return proxy_client