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
fastmcp/server/proxy.py CHANGED
@@ -1,707 +1,41 @@
1
+ """Backwards compatibility - import from fastmcp.server.providers.proxy instead.
2
+
3
+ This module re-exports all proxy-related classes from their new location
4
+ at fastmcp.server.providers.proxy. Direct imports from this module are
5
+ deprecated and will be removed in a future version.
6
+ """
7
+
1
8
  from __future__ import annotations
2
9
 
3
- import inspect
4
- from collections.abc import Awaitable, Callable
5
- from pathlib import Path
6
- from typing import TYPE_CHECKING, Any, cast
7
- from urllib.parse import quote
10
+ import warnings
8
11
 
9
- import mcp.types
10
- from mcp import ServerSession
11
- from mcp.client.session import ClientSession
12
- from mcp.shared.context import LifespanContextT, RequestContext
13
- from mcp.shared.exceptions import McpError
14
- from mcp.types import (
15
- METHOD_NOT_FOUND,
16
- BlobResourceContents,
17
- ElicitRequestFormParams,
18
- GetPromptResult,
19
- TextResourceContents,
12
+ warnings.warn(
13
+ "fastmcp.server.proxy is deprecated. Use fastmcp.server.providers.proxy instead.",
14
+ DeprecationWarning,
15
+ stacklevel=2,
20
16
  )
21
- from pydantic.networks import AnyUrl
22
17
 
23
- from fastmcp.client.client import Client, FastMCP1Server
24
- from fastmcp.client.elicitation import ElicitResult
25
- from fastmcp.client.logging import LogMessage
26
- from fastmcp.client.roots import RootsList
27
- from fastmcp.client.transports import ClientTransportT
28
- from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
29
- from fastmcp.mcp_config import MCPConfig
30
- from fastmcp.prompts import Prompt, PromptMessage
31
- from fastmcp.prompts.prompt import PromptArgument
32
- from fastmcp.prompts.prompt_manager import PromptManager
33
- from fastmcp.resources import Resource, ResourceTemplate
34
- from fastmcp.resources.resource_manager import ResourceManager
35
- from fastmcp.server.context import Context
36
- from fastmcp.server.dependencies import get_context
37
- from fastmcp.server.server import FastMCP
38
- from fastmcp.server.tasks.config import TaskConfig
39
- from fastmcp.tools.tool import Tool, ToolResult
40
- from fastmcp.tools.tool_manager import ToolManager
41
- from fastmcp.tools.tool_transform import (
42
- apply_transformations_to_tools,
18
+ # Re-export everything from the new location
19
+ from fastmcp.server.providers.proxy import ( # noqa: E402
20
+ ClientFactoryT,
21
+ FastMCPProxy,
22
+ ProxyClient,
23
+ ProxyPrompt,
24
+ ProxyProvider,
25
+ ProxyResource,
26
+ ProxyTemplate,
27
+ ProxyTool,
28
+ StatefulProxyClient,
43
29
  )
44
- from fastmcp.utilities.components import MirroredComponent
45
- from fastmcp.utilities.logging import get_logger
46
-
47
- if TYPE_CHECKING:
48
- from fastmcp.server import Context
49
-
50
- logger = get_logger(__name__)
51
-
52
- # Type alias for client factory functions
53
- ClientFactoryT = Callable[[], Client] | Callable[[], Awaitable[Client]]
54
-
55
-
56
- class ProxyManagerMixin:
57
- """A mixin for proxy managers to provide a unified client retrieval method."""
58
-
59
- client_factory: ClientFactoryT
60
-
61
- async def _get_client(self) -> Client:
62
- """Gets a client instance by calling the sync or async factory."""
63
- client = self.client_factory()
64
- if inspect.isawaitable(client):
65
- client = await client
66
- return client
67
-
68
-
69
- class ProxyToolManager(ToolManager, ProxyManagerMixin):
70
- """A ToolManager that sources its tools from a remote client in addition to local and mounted tools."""
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_tools(self) -> dict[str, Tool]:
77
- """Gets the unfiltered tool inventory including local, mounted, and proxy tools."""
78
- # First get local and mounted tools from parent
79
- all_tools = await super().get_tools()
80
-
81
- # Then add proxy tools, but don't overwrite existing ones
82
- try:
83
- client = await self._get_client()
84
- async with client:
85
- client_tools = await client.list_tools()
86
- for tool in client_tools:
87
- if tool.name not in all_tools:
88
- all_tools[tool.name] = ProxyTool.from_mcp_tool(client, tool)
89
- except McpError as e:
90
- if e.error.code == METHOD_NOT_FOUND:
91
- pass # No tools available from proxy
92
- else:
93
- raise e
94
-
95
- transformed_tools = apply_transformations_to_tools(
96
- tools=all_tools,
97
- transformations=self.transformations,
98
- )
99
-
100
- return transformed_tools
101
-
102
- async def list_tools(self) -> list[Tool]:
103
- """Gets the filtered list of tools including local, mounted, and proxy tools."""
104
- tools_dict = await self.get_tools()
105
- return list(tools_dict.values())
106
-
107
- async def call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
108
- """Calls a tool, trying local/mounted first, then proxy if not found."""
109
- try:
110
- # First try local and mounted tools
111
- return await super().call_tool(key, arguments)
112
- except NotFoundError:
113
- # If not found locally, try proxy
114
- client = await self._get_client()
115
- async with client:
116
- result = await client.call_tool(key, arguments)
117
- return ToolResult(
118
- content=result.content,
119
- structured_content=result.structured_content,
120
- meta=result.meta,
121
- )
122
-
123
-
124
- class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
125
- """A ResourceManager that sources its resources from a remote client in addition to local and mounted resources."""
126
-
127
- def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
128
- super().__init__(**kwargs)
129
- self.client_factory = client_factory
130
-
131
- async def get_resources(self) -> dict[str, Resource]:
132
- """Gets the unfiltered resource inventory including local, mounted, and proxy resources."""
133
- # First get local and mounted resources from parent
134
- all_resources = await super().get_resources()
135
-
136
- # Then add proxy resources, but don't overwrite existing ones
137
- try:
138
- client = await self._get_client()
139
- async with client:
140
- client_resources = await client.list_resources()
141
- for resource in client_resources:
142
- if str(resource.uri) not in all_resources:
143
- all_resources[str(resource.uri)] = (
144
- ProxyResource.from_mcp_resource(client, resource)
145
- )
146
- except McpError as e:
147
- if e.error.code == METHOD_NOT_FOUND:
148
- pass # No resources available from proxy
149
- else:
150
- raise e
151
-
152
- return all_resources
153
-
154
- async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
155
- """Gets the unfiltered template inventory including local, mounted, and proxy templates."""
156
- # First get local and mounted templates from parent
157
- all_templates = await super().get_resource_templates()
158
-
159
- # Then add proxy templates, but don't overwrite existing ones
160
- try:
161
- client = await self._get_client()
162
- async with client:
163
- client_templates = await client.list_resource_templates()
164
- for template in client_templates:
165
- if template.uriTemplate not in all_templates:
166
- all_templates[template.uriTemplate] = (
167
- ProxyTemplate.from_mcp_template(client, template)
168
- )
169
- except McpError as e:
170
- if e.error.code == METHOD_NOT_FOUND:
171
- pass # No templates available from proxy
172
- else:
173
- raise e
174
-
175
- return all_templates
176
-
177
- async def list_resources(self) -> list[Resource]:
178
- """Gets the filtered list of resources including local, mounted, and proxy resources."""
179
- resources_dict = await self.get_resources()
180
- return list(resources_dict.values())
181
-
182
- async def list_resource_templates(self) -> list[ResourceTemplate]:
183
- """Gets the filtered list of templates including local, mounted, and proxy templates."""
184
- templates_dict = await self.get_resource_templates()
185
- return list(templates_dict.values())
186
-
187
- async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
188
- """Reads a resource, trying local/mounted first, then proxy if not found."""
189
- try:
190
- # First try local and mounted resources
191
- return await super().read_resource(uri)
192
- except NotFoundError:
193
- # If not found locally, try proxy
194
- client = await self._get_client()
195
- async with client:
196
- result = await client.read_resource(uri)
197
- if isinstance(result[0], TextResourceContents):
198
- return result[0].text
199
- elif isinstance(result[0], BlobResourceContents):
200
- return result[0].blob
201
- else:
202
- raise ResourceError(
203
- f"Unsupported content type: {type(result[0])}"
204
- ) from None
205
-
206
-
207
- class ProxyPromptManager(PromptManager, ProxyManagerMixin):
208
- """A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts."""
209
-
210
- def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
211
- super().__init__(**kwargs)
212
- self.client_factory = client_factory
213
-
214
- async def get_prompts(self) -> dict[str, Prompt]:
215
- """Gets the unfiltered prompt inventory including local, mounted, and proxy prompts."""
216
- # First get local and mounted prompts from parent
217
- all_prompts = await super().get_prompts()
218
-
219
- # Then add proxy prompts, but don't overwrite existing ones
220
- try:
221
- client = await self._get_client()
222
- async with client:
223
- client_prompts = await client.list_prompts()
224
- for prompt in client_prompts:
225
- if prompt.name not in all_prompts:
226
- all_prompts[prompt.name] = ProxyPrompt.from_mcp_prompt(
227
- client, prompt
228
- )
229
- except McpError as e:
230
- if e.error.code == METHOD_NOT_FOUND:
231
- pass # No prompts available from proxy
232
- else:
233
- raise e
234
-
235
- return all_prompts
236
-
237
- async def list_prompts(self) -> list[Prompt]:
238
- """Gets the filtered list of prompts including local, mounted, and proxy prompts."""
239
- prompts_dict = await self.get_prompts()
240
- return list(prompts_dict.values())
241
-
242
- async def render_prompt(
243
- self,
244
- name: str,
245
- arguments: dict[str, Any] | None = None,
246
- ) -> GetPromptResult:
247
- """Renders a prompt, trying local/mounted first, then proxy if not found."""
248
- try:
249
- # First try local and mounted prompts
250
- return await super().render_prompt(name, arguments)
251
- except NotFoundError:
252
- # If not found locally, try proxy
253
- client = await self._get_client()
254
- async with client:
255
- result = await client.get_prompt(name, arguments)
256
- return result
257
-
258
-
259
- class ProxyTool(Tool, MirroredComponent):
260
- """
261
- A Tool that represents and executes a tool on a remote server.
262
- """
263
-
264
- task_config: TaskConfig = TaskConfig(mode="forbidden")
265
-
266
- def __init__(self, client: Client, **kwargs: Any):
267
- super().__init__(**kwargs)
268
- self._client = client
269
-
270
- @classmethod
271
- def from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool:
272
- """Factory method to create a ProxyTool from a raw MCP tool schema."""
273
- return cls(
274
- client=client,
275
- name=mcp_tool.name,
276
- title=mcp_tool.title,
277
- description=mcp_tool.description,
278
- parameters=mcp_tool.inputSchema,
279
- annotations=mcp_tool.annotations,
280
- output_schema=mcp_tool.outputSchema,
281
- icons=mcp_tool.icons,
282
- meta=mcp_tool.meta,
283
- tags=(mcp_tool.meta or {}).get("_fastmcp", {}).get("tags", []),
284
- _mirrored=True,
285
- )
286
-
287
- async def run(
288
- self,
289
- arguments: dict[str, Any],
290
- context: Context | None = None,
291
- ) -> ToolResult:
292
- """Executes the tool by making a call through the client."""
293
- async with self._client:
294
- context = get_context()
295
- # Build meta dict from request context
296
- meta: dict[str, Any] | None = None
297
- if hasattr(context, "request_context"):
298
- req_ctx = context.request_context
299
- # Start with existing meta if present
300
- if hasattr(req_ctx, "meta") and req_ctx.meta:
301
- meta = dict(req_ctx.meta)
302
- # Add task metadata if this is a task request
303
- if (
304
- hasattr(req_ctx, "experimental")
305
- and hasattr(req_ctx.experimental, "is_task")
306
- and req_ctx.experimental.is_task
307
- ):
308
- task_metadata = req_ctx.experimental.task_metadata
309
- if task_metadata:
310
- meta = meta or {}
311
- meta["modelcontextprotocol.io/task"] = task_metadata.model_dump(
312
- exclude_none=True
313
- )
314
-
315
- result = await self._client.call_tool_mcp(
316
- name=self.name, arguments=arguments, meta=meta
317
- )
318
- if result.isError:
319
- raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
320
- # Preserve backend's meta (includes task metadata for background tasks)
321
- return ToolResult(
322
- content=result.content,
323
- structured_content=result.structuredContent,
324
- meta=result.meta,
325
- )
326
-
327
-
328
- class ProxyResource(Resource, MirroredComponent):
329
- """
330
- A Resource that represents and reads a resource from a remote server.
331
- """
332
-
333
- task_config: TaskConfig = TaskConfig(mode="forbidden")
334
- _client: Client
335
- _value: str | bytes | None = None
336
-
337
- def __init__(
338
- self,
339
- client: Client,
340
- *,
341
- _value: str | bytes | None = None,
342
- **kwargs,
343
- ):
344
- super().__init__(**kwargs)
345
- self._client = client
346
- self._value = _value
347
-
348
- @classmethod
349
- def from_mcp_resource(
350
- cls,
351
- client: Client,
352
- mcp_resource: mcp.types.Resource,
353
- ) -> ProxyResource:
354
- """Factory method to create a ProxyResource from a raw MCP resource schema."""
355
-
356
- return cls(
357
- client=client,
358
- uri=mcp_resource.uri,
359
- name=mcp_resource.name,
360
- title=mcp_resource.title,
361
- description=mcp_resource.description,
362
- mime_type=mcp_resource.mimeType or "text/plain",
363
- icons=mcp_resource.icons,
364
- meta=mcp_resource.meta,
365
- tags=(mcp_resource.meta or {}).get("_fastmcp", {}).get("tags", []),
366
- task_config=TaskConfig(mode="forbidden"),
367
- _mirrored=True,
368
- )
369
-
370
- async def read(self) -> str | bytes:
371
- """Read the resource content from the remote server."""
372
- if self._value is not None:
373
- return self._value
374
-
375
- async with self._client:
376
- result = await self._client.read_resource(self.uri)
377
- if isinstance(result[0], TextResourceContents):
378
- return result[0].text
379
- elif isinstance(result[0], BlobResourceContents):
380
- return result[0].blob
381
- else:
382
- raise ResourceError(f"Unsupported content type: {type(result[0])}")
383
-
384
-
385
- class ProxyTemplate(ResourceTemplate, MirroredComponent):
386
- """
387
- A ResourceTemplate that represents and creates resources from a remote server template.
388
- """
389
-
390
- task_config: TaskConfig = TaskConfig(mode="forbidden")
391
-
392
- def __init__(self, client: Client, **kwargs: Any):
393
- super().__init__(**kwargs)
394
- self._client = client
395
-
396
- @classmethod
397
- def from_mcp_template( # type: ignore[override]
398
- cls, client: Client, mcp_template: mcp.types.ResourceTemplate
399
- ) -> ProxyTemplate:
400
- """Factory method to create a ProxyTemplate from a raw MCP template schema."""
401
- return cls(
402
- client=client,
403
- uri_template=mcp_template.uriTemplate,
404
- name=mcp_template.name,
405
- title=mcp_template.title,
406
- description=mcp_template.description,
407
- mime_type=mcp_template.mimeType or "text/plain",
408
- icons=mcp_template.icons,
409
- parameters={}, # Remote templates don't have local parameters
410
- meta=mcp_template.meta,
411
- tags=(mcp_template.meta or {}).get("_fastmcp", {}).get("tags", []),
412
- task_config=TaskConfig(mode="forbidden"),
413
- _mirrored=True,
414
- )
415
-
416
- async def create_resource(
417
- self,
418
- uri: str,
419
- params: dict[str, Any],
420
- context: Context | None = None,
421
- ) -> ProxyResource:
422
- """Create a resource from the template by calling the remote server."""
423
- # don't use the provided uri, because it may not be the same as the
424
- # uri_template on the remote server.
425
- # quote params to ensure they are valid for the uri_template
426
- parameterized_uri = self.uri_template.format(
427
- **{k: quote(v, safe="") for k, v in params.items()}
428
- )
429
- async with self._client:
430
- result = await self._client.read_resource(parameterized_uri)
431
-
432
- if isinstance(result[0], TextResourceContents):
433
- value = result[0].text
434
- elif isinstance(result[0], BlobResourceContents):
435
- value = result[0].blob
436
- else:
437
- raise ResourceError(f"Unsupported content type: {type(result[0])}")
438
-
439
- return ProxyResource(
440
- client=self._client,
441
- uri=parameterized_uri,
442
- name=self.name,
443
- title=self.title,
444
- description=self.description,
445
- mime_type=result[0].mimeType,
446
- icons=self.icons,
447
- meta=self.meta,
448
- tags=(self.meta or {}).get("_fastmcp", {}).get("tags", []),
449
- _value=value,
450
- )
451
-
452
-
453
- class ProxyPrompt(Prompt, MirroredComponent):
454
- """
455
- A Prompt that represents and renders a prompt from a remote server.
456
- """
457
-
458
- task_config: TaskConfig = TaskConfig(mode="forbidden")
459
- _client: Client
460
-
461
- def __init__(self, client: Client, **kwargs):
462
- super().__init__(**kwargs)
463
- self._client = client
464
-
465
- @classmethod
466
- def from_mcp_prompt(
467
- cls, client: Client, mcp_prompt: mcp.types.Prompt
468
- ) -> ProxyPrompt:
469
- """Factory method to create a ProxyPrompt from a raw MCP prompt schema."""
470
- arguments = [
471
- PromptArgument(
472
- name=arg.name,
473
- description=arg.description,
474
- required=arg.required or False,
475
- )
476
- for arg in mcp_prompt.arguments or []
477
- ]
478
- return cls(
479
- client=client,
480
- name=mcp_prompt.name,
481
- title=mcp_prompt.title,
482
- description=mcp_prompt.description,
483
- arguments=arguments,
484
- icons=mcp_prompt.icons,
485
- meta=mcp_prompt.meta,
486
- tags=(mcp_prompt.meta or {}).get("_fastmcp", {}).get("tags", []),
487
- task_config=TaskConfig(mode="forbidden"),
488
- _mirrored=True,
489
- )
490
-
491
- async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]: # type: ignore[override]
492
- """Render the prompt by making a call through the client."""
493
- async with self._client:
494
- result = await self._client.get_prompt(self.name, arguments)
495
- return result.messages
496
-
497
-
498
- class FastMCPProxy(FastMCP):
499
- """
500
- A FastMCP server that acts as a proxy to a remote MCP-compliant server.
501
- It uses specialized managers that fulfill requests via a client factory.
502
- """
503
-
504
- def __init__(
505
- self,
506
- *,
507
- client_factory: ClientFactoryT,
508
- **kwargs,
509
- ):
510
- """
511
- Initializes the proxy server.
512
-
513
- FastMCPProxy requires explicit session management via client_factory.
514
- Use FastMCP.as_proxy() for convenience with automatic session strategy.
515
-
516
- Args:
517
- client_factory: A callable that returns a Client instance when called.
518
- This gives you full control over session creation and reuse.
519
- Can be either a synchronous or asynchronous function.
520
- **kwargs: Additional settings for the FastMCP server.
521
- """
522
-
523
- super().__init__(**kwargs)
524
-
525
- self.client_factory = client_factory
526
-
527
- # Replace the default managers with our specialized proxy managers.
528
- self._tool_manager = ProxyToolManager(
529
- client_factory=self.client_factory,
530
- # Propagate the transformations from the base class tool manager
531
- transformations=self._tool_manager.transformations,
532
- )
533
- self._resource_manager = ProxyResourceManager(
534
- client_factory=self.client_factory
535
- )
536
- self._prompt_manager = ProxyPromptManager(client_factory=self.client_factory)
537
-
538
-
539
- async def default_proxy_roots_handler(
540
- context: RequestContext[ClientSession, LifespanContextT],
541
- ) -> RootsList:
542
- """
543
- A handler that forwards the list roots request from the remote server to the proxy's connected clients and relays the response back to the remote server.
544
- """
545
- ctx = get_context()
546
- return await ctx.list_roots()
547
-
548
-
549
- class ProxyClient(Client[ClientTransportT]):
550
- """
551
- A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
552
- Supports forwarding roots, sampling, elicitation, logging, and progress.
553
- """
554
-
555
- def __init__(
556
- self,
557
- transport: ClientTransportT
558
- | FastMCP[Any]
559
- | FastMCP1Server
560
- | AnyUrl
561
- | Path
562
- | MCPConfig
563
- | dict[str, Any]
564
- | str,
565
- **kwargs,
566
- ):
567
- if "name" not in kwargs:
568
- kwargs["name"] = self.generate_name()
569
- if "roots" not in kwargs:
570
- kwargs["roots"] = default_proxy_roots_handler
571
- if "sampling_handler" not in kwargs:
572
- kwargs["sampling_handler"] = ProxyClient.default_sampling_handler
573
- if "elicitation_handler" not in kwargs:
574
- kwargs["elicitation_handler"] = ProxyClient.default_elicitation_handler
575
- if "log_handler" not in kwargs:
576
- kwargs["log_handler"] = ProxyClient.default_log_handler
577
- if "progress_handler" not in kwargs:
578
- kwargs["progress_handler"] = ProxyClient.default_progress_handler
579
- super().__init__(**kwargs | {"transport": transport})
580
-
581
- @classmethod
582
- async def default_sampling_handler(
583
- cls,
584
- messages: list[mcp.types.SamplingMessage],
585
- params: mcp.types.CreateMessageRequestParams,
586
- context: RequestContext[ClientSession, LifespanContextT],
587
- ) -> mcp.types.CreateMessageResult:
588
- """
589
- A handler that forwards the sampling request from the remote server to the proxy's connected clients and relays the response back to the remote server.
590
- """
591
- ctx = get_context()
592
- result = await ctx.sample(
593
- list(messages),
594
- system_prompt=params.systemPrompt,
595
- temperature=params.temperature,
596
- max_tokens=params.maxTokens,
597
- model_preferences=params.modelPreferences,
598
- )
599
- # Create TextContent from the result text
600
- content = mcp.types.TextContent(type="text", text=result.text or "")
601
- return mcp.types.CreateMessageResult(
602
- role="assistant",
603
- model="fastmcp-client",
604
- # TODO(ty): remove when ty supports isinstance exclusion narrowing
605
- content=content, # type: ignore[arg-type]
606
- )
607
-
608
- @classmethod
609
- async def default_elicitation_handler(
610
- cls,
611
- message: str,
612
- response_type: type,
613
- params: mcp.types.ElicitRequestParams,
614
- context: RequestContext[ClientSession, LifespanContextT],
615
- ) -> ElicitResult:
616
- """
617
- A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server.
618
- """
619
- ctx = get_context()
620
- # requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams
621
- requested_schema = (
622
- params.requestedSchema
623
- if isinstance(params, ElicitRequestFormParams)
624
- else {"type": "object", "properties": {}}
625
- )
626
- result = await ctx.session.elicit(
627
- message=message,
628
- requestedSchema=requested_schema,
629
- related_request_id=ctx.request_id,
630
- )
631
- return ElicitResult(action=result.action, content=result.content)
632
-
633
- @classmethod
634
- async def default_log_handler(cls, message: LogMessage) -> None:
635
- """
636
- A handler that forwards the log notification from the remote server to the proxy's connected clients.
637
- """
638
- ctx = get_context()
639
- msg = message.data.get("msg")
640
- extra = message.data.get("extra")
641
- await ctx.log(msg, level=message.level, logger_name=message.logger, extra=extra)
642
-
643
- @classmethod
644
- async def default_progress_handler(
645
- cls,
646
- progress: float,
647
- total: float | None,
648
- message: str | None,
649
- ) -> None:
650
- """
651
- A handler that forwards the progress notification from the remote server to the proxy's connected clients.
652
- """
653
- ctx = get_context()
654
- await ctx.report_progress(progress, total, message)
655
-
656
-
657
- class StatefulProxyClient(ProxyClient[ClientTransportT]):
658
- """
659
- A proxy client that provides a stateful client factory for the proxy server.
660
-
661
- The stateful proxy client bound its copy to the server session.
662
- And it will be disconnected when the session is exited.
663
-
664
- This is useful to proxy a stateful mcp server such as the Playwright MCP server.
665
- Note that it is essential to ensure that the proxy server itself is also stateful.
666
- """
667
-
668
- def __init__(self, *args: Any, **kwargs: Any):
669
- super().__init__(*args, **kwargs)
670
- self._caches: dict[ServerSession, Client[ClientTransportT]] = {}
671
-
672
- async def __aexit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[override]
673
- """
674
- The stateful proxy client will be forced disconnected when the session is exited.
675
- So we do nothing here.
676
- """
677
-
678
- async def clear(self):
679
- """
680
- Clear all cached clients and force disconnect them.
681
- """
682
- while self._caches:
683
- _, cache = self._caches.popitem()
684
- await cache._disconnect(force=True)
685
-
686
- def new_stateful(self) -> Client[ClientTransportT]:
687
- """
688
- Create a new stateful proxy client instance with the same configuration.
689
-
690
- Use this method as the client factory for stateful proxy server.
691
- """
692
- session = get_context().session
693
- proxy_client = self._caches.get(session, None)
694
-
695
- if proxy_client is None:
696
- proxy_client = self.new()
697
- logger.debug(f"{proxy_client} created for {session}")
698
- self._caches[session] = proxy_client
699
-
700
- async def _on_session_exit():
701
- self._caches.pop(session)
702
- logger.debug(f"{proxy_client} will be disconnect")
703
- await proxy_client._disconnect(force=True)
704
-
705
- session._exit_stack.push_async_callback(_on_session_exit)
706
30
 
707
- return proxy_client
31
+ __all__ = [
32
+ "ClientFactoryT",
33
+ "FastMCPProxy",
34
+ "ProxyClient",
35
+ "ProxyPrompt",
36
+ "ProxyProvider",
37
+ "ProxyResource",
38
+ "ProxyTemplate",
39
+ "ProxyTool",
40
+ "StatefulProxyClient",
41
+ ]