fastmcp 2.10.5__py3-none-any.whl → 2.11.0__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/__init__.py +7 -2
- fastmcp/cli/cli.py +128 -33
- fastmcp/cli/install/__init__.py +2 -2
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/{mcp_config.py → mcp_json.py} +51 -7
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/prompts/prompt_manager.py +1 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/resource_manager.py +2 -2
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +17 -2
- fastmcp/server/auth/auth.py +144 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +170 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +110 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +62 -30
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +26 -13
- fastmcp/server/proxy.py +89 -8
- fastmcp/server/server.py +170 -62
- fastmcp/settings.py +83 -18
- fastmcp/tools/tool.py +41 -6
- fastmcp/tools/tool_manager.py +39 -3
- fastmcp/tools/tool_transform.py +122 -6
- fastmcp/utilities/components.py +35 -2
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +306 -30
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +89 -11
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
- fastmcp-2.11.0.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.5.dist-info/RECORD +0 -93
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/http.py
CHANGED
|
@@ -3,18 +3,20 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import AsyncGenerator, Callable, Generator
|
|
4
4
|
from contextlib import asynccontextmanager, contextmanager
|
|
5
5
|
from contextvars import ContextVar
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
7
|
|
|
8
8
|
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
|
9
9
|
from mcp.server.auth.middleware.bearer_auth import (
|
|
10
10
|
BearerAuthBackend,
|
|
11
11
|
RequireAuthMiddleware,
|
|
12
12
|
)
|
|
13
|
+
from mcp.server.auth.provider import TokenVerifier as TokenVerifierProtocol
|
|
13
14
|
from mcp.server.auth.routes import create_auth_routes
|
|
14
15
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
15
16
|
from mcp.server.sse import SseServerTransport
|
|
16
17
|
from mcp.server.streamable_http import EventStore
|
|
17
18
|
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
19
|
+
from pydantic import AnyHttpUrl
|
|
18
20
|
from starlette.applications import Starlette
|
|
19
21
|
from starlette.middleware import Middleware
|
|
20
22
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
@@ -23,7 +25,7 @@ from starlette.responses import Response
|
|
|
23
25
|
from starlette.routing import BaseRoute, Mount, Route
|
|
24
26
|
from starlette.types import Lifespan, Receive, Scope, Send
|
|
25
27
|
|
|
26
|
-
from fastmcp.server.auth.auth import OAuthProvider
|
|
28
|
+
from fastmcp.server.auth.auth import AuthProvider, OAuthProvider, TokenVerifier
|
|
27
29
|
from fastmcp.utilities.logging import get_logger
|
|
28
30
|
|
|
29
31
|
if TYPE_CHECKING:
|
|
@@ -70,39 +72,46 @@ class RequestContextMiddleware:
|
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
def setup_auth_middleware_and_routes(
|
|
73
|
-
auth:
|
|
74
|
-
) -> tuple[list[Middleware], list[
|
|
75
|
+
auth: AuthProvider,
|
|
76
|
+
) -> tuple[list[Middleware], list[Route], list[str]]:
|
|
75
77
|
"""Set up authentication middleware and routes if auth is enabled.
|
|
76
78
|
|
|
77
79
|
Args:
|
|
78
|
-
auth:
|
|
80
|
+
auth: An AuthProvider for authentication (TokenVerifier or OAuthProvider)
|
|
79
81
|
|
|
80
82
|
Returns:
|
|
81
83
|
Tuple of (middleware, auth_routes, required_scopes)
|
|
82
84
|
"""
|
|
83
|
-
middleware: list[Middleware] = [
|
|
84
|
-
auth_routes: list[BaseRoute] = []
|
|
85
|
-
required_scopes: list[str] = []
|
|
86
|
-
|
|
87
|
-
middleware = [
|
|
85
|
+
middleware: list[Middleware] = [
|
|
88
86
|
Middleware(
|
|
89
87
|
AuthenticationMiddleware,
|
|
90
|
-
backend=BearerAuthBackend(auth),
|
|
88
|
+
backend=BearerAuthBackend(cast(TokenVerifierProtocol, auth)),
|
|
91
89
|
),
|
|
92
90
|
Middleware(AuthContextMiddleware),
|
|
93
91
|
]
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
auth_routes: list[Route] = []
|
|
94
|
+
required_scopes: list[str] = auth.required_scopes or []
|
|
95
|
+
|
|
96
|
+
# Check if it's an OAuthProvider (has OAuth server capability)
|
|
97
|
+
if isinstance(auth, OAuthProvider):
|
|
98
|
+
# OAuthProvider: create standard OAuth routes first
|
|
99
|
+
standard_routes = list(
|
|
100
|
+
create_auth_routes(
|
|
101
|
+
provider=auth,
|
|
102
|
+
issuer_url=auth.issuer_url,
|
|
103
|
+
service_documentation_url=auth.service_documentation_url,
|
|
104
|
+
client_registration_options=auth.client_registration_options,
|
|
105
|
+
revocation_options=auth.revocation_options,
|
|
106
|
+
)
|
|
104
107
|
)
|
|
105
|
-
|
|
108
|
+
|
|
109
|
+
# Allow provider to customize routes (e.g., for proxy behavior or metadata endpoints)
|
|
110
|
+
auth_routes = auth.customize_auth_routes(standard_routes)
|
|
111
|
+
else:
|
|
112
|
+
# Simple AuthProvider or TokenVerifier: start with empty routes
|
|
113
|
+
# Allow provider to add custom routes (e.g., metadata endpoints)
|
|
114
|
+
auth_routes = auth.customize_auth_routes([])
|
|
106
115
|
|
|
107
116
|
return middleware, auth_routes, required_scopes
|
|
108
117
|
|
|
@@ -139,7 +148,7 @@ def create_sse_app(
|
|
|
139
148
|
server: FastMCP[LifespanResultT],
|
|
140
149
|
message_path: str,
|
|
141
150
|
sse_path: str,
|
|
142
|
-
auth:
|
|
151
|
+
auth: AuthProvider | None = None,
|
|
143
152
|
debug: bool = False,
|
|
144
153
|
routes: list[BaseRoute] | None = None,
|
|
145
154
|
middleware: list[Middleware] | None = None,
|
|
@@ -150,7 +159,7 @@ def create_sse_app(
|
|
|
150
159
|
server: The FastMCP server instance
|
|
151
160
|
message_path: Path for SSE messages
|
|
152
161
|
sse_path: Path for SSE connections
|
|
153
|
-
auth: Optional
|
|
162
|
+
auth: Optional authentication provider (AuthProvider)
|
|
154
163
|
debug: Whether to enable debug mode
|
|
155
164
|
routes: Optional list of custom routes
|
|
156
165
|
middleware: Optional list of middleware
|
|
@@ -175,8 +184,6 @@ def create_sse_app(
|
|
|
175
184
|
return Response()
|
|
176
185
|
|
|
177
186
|
# Get auth middleware and routes
|
|
178
|
-
|
|
179
|
-
# Add SSE routes with or without auth
|
|
180
187
|
if auth:
|
|
181
188
|
auth_middleware, auth_routes, required_scopes = (
|
|
182
189
|
setup_auth_middleware_and_routes(auth)
|
|
@@ -184,18 +191,32 @@ def create_sse_app(
|
|
|
184
191
|
|
|
185
192
|
server_routes.extend(auth_routes)
|
|
186
193
|
server_middleware.extend(auth_middleware)
|
|
194
|
+
|
|
195
|
+
# Determine resource_metadata_url for TokenVerifier
|
|
196
|
+
resource_metadata_url = None
|
|
197
|
+
if isinstance(auth, TokenVerifier) and auth.resource_server_url:
|
|
198
|
+
# Add .well-known path for RFC 9728 compliance
|
|
199
|
+
resource_metadata_url = AnyHttpUrl(
|
|
200
|
+
str(auth.resource_server_url).rstrip("/")
|
|
201
|
+
+ "/.well-known/oauth-protected-resource"
|
|
202
|
+
)
|
|
203
|
+
|
|
187
204
|
# Auth is enabled, wrap endpoints with RequireAuthMiddleware
|
|
188
205
|
server_routes.append(
|
|
189
206
|
Route(
|
|
190
207
|
sse_path,
|
|
191
|
-
endpoint=RequireAuthMiddleware(
|
|
208
|
+
endpoint=RequireAuthMiddleware(
|
|
209
|
+
handle_sse, required_scopes, resource_metadata_url
|
|
210
|
+
),
|
|
192
211
|
methods=["GET"],
|
|
193
212
|
)
|
|
194
213
|
)
|
|
195
214
|
server_routes.append(
|
|
196
215
|
Mount(
|
|
197
216
|
message_path,
|
|
198
|
-
app=RequireAuthMiddleware(
|
|
217
|
+
app=RequireAuthMiddleware(
|
|
218
|
+
sse.handle_post_message, required_scopes, resource_metadata_url
|
|
219
|
+
),
|
|
199
220
|
)
|
|
200
221
|
)
|
|
201
222
|
else:
|
|
@@ -243,7 +264,7 @@ def create_streamable_http_app(
|
|
|
243
264
|
server: FastMCP[LifespanResultT],
|
|
244
265
|
streamable_http_path: str,
|
|
245
266
|
event_store: EventStore | None = None,
|
|
246
|
-
auth:
|
|
267
|
+
auth: AuthProvider | None = None,
|
|
247
268
|
json_response: bool = False,
|
|
248
269
|
stateless_http: bool = False,
|
|
249
270
|
debug: bool = False,
|
|
@@ -256,7 +277,7 @@ def create_streamable_http_app(
|
|
|
256
277
|
server: The FastMCP server instance
|
|
257
278
|
streamable_http_path: Path for StreamableHTTP connections
|
|
258
279
|
event_store: Optional event store for session management
|
|
259
|
-
auth: Optional
|
|
280
|
+
auth: Optional authentication provider (AuthProvider)
|
|
260
281
|
json_response: Whether to use JSON response format
|
|
261
282
|
stateless_http: Whether to use stateless mode (new transport per request)
|
|
262
283
|
debug: Whether to enable debug mode
|
|
@@ -314,11 +335,22 @@ def create_streamable_http_app(
|
|
|
314
335
|
server_routes.extend(auth_routes)
|
|
315
336
|
server_middleware.extend(auth_middleware)
|
|
316
337
|
|
|
338
|
+
# Determine resource_metadata_url for TokenVerifier
|
|
339
|
+
resource_metadata_url = None
|
|
340
|
+
if isinstance(auth, TokenVerifier) and auth.resource_server_url:
|
|
341
|
+
# Add .well-known path for RFC 9728 compliance
|
|
342
|
+
resource_metadata_url = AnyHttpUrl(
|
|
343
|
+
str(auth.resource_server_url).rstrip("/")
|
|
344
|
+
+ "/.well-known/oauth-protected-resource"
|
|
345
|
+
)
|
|
346
|
+
|
|
317
347
|
# Auth is enabled, wrap endpoint with RequireAuthMiddleware
|
|
318
348
|
server_routes.append(
|
|
319
349
|
Mount(
|
|
320
350
|
streamable_http_path,
|
|
321
|
-
app=RequireAuthMiddleware(
|
|
351
|
+
app=RequireAuthMiddleware(
|
|
352
|
+
handle_streamable_http, required_scopes, resource_metadata_url
|
|
353
|
+
),
|
|
322
354
|
)
|
|
323
355
|
)
|
|
324
356
|
else:
|
|
@@ -20,7 +20,7 @@ import mcp.types as mt
|
|
|
20
20
|
from fastmcp.prompts.prompt import Prompt
|
|
21
21
|
from fastmcp.resources.resource import Resource
|
|
22
22
|
from fastmcp.resources.template import ResourceTemplate
|
|
23
|
-
from fastmcp.tools.tool import Tool
|
|
23
|
+
from fastmcp.tools.tool import Tool, ToolResult
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from fastmcp.server.context import Context
|
|
@@ -43,26 +43,6 @@ class CallNext(Protocol[T, R]):
|
|
|
43
43
|
def __call__(self, context: MiddlewareContext[T]) -> Awaitable[R]: ...
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
ServerResultT = TypeVar(
|
|
47
|
-
"ServerResultT",
|
|
48
|
-
bound=mt.EmptyResult
|
|
49
|
-
| mt.InitializeResult
|
|
50
|
-
| mt.CompleteResult
|
|
51
|
-
| mt.GetPromptResult
|
|
52
|
-
| mt.ListPromptsResult
|
|
53
|
-
| mt.ListResourcesResult
|
|
54
|
-
| mt.ListResourceTemplatesResult
|
|
55
|
-
| mt.ReadResourceResult
|
|
56
|
-
| mt.CallToolResult
|
|
57
|
-
| mt.ListToolsResult,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@runtime_checkable
|
|
62
|
-
class ServerResultProtocol(Protocol[ServerResultT]):
|
|
63
|
-
root: ServerResultT
|
|
64
|
-
|
|
65
|
-
|
|
66
46
|
@dataclass(kw_only=True, frozen=True)
|
|
67
47
|
class MiddlewareContext(Generic[T]):
|
|
68
48
|
"""
|
|
@@ -167,8 +147,8 @@ class Middleware:
|
|
|
167
147
|
async def on_call_tool(
|
|
168
148
|
self,
|
|
169
149
|
context: MiddlewareContext[mt.CallToolRequestParams],
|
|
170
|
-
call_next: CallNext[mt.CallToolRequestParams,
|
|
171
|
-
) ->
|
|
150
|
+
call_next: CallNext[mt.CallToolRequestParams, ToolResult],
|
|
151
|
+
) -> ToolResult:
|
|
172
152
|
return await call_next(context)
|
|
173
153
|
|
|
174
154
|
async def on_read_resource(
|
fastmcp/server/openapi.py
CHANGED
|
@@ -344,18 +344,28 @@ class OpenAPITool(Tool):
|
|
|
344
344
|
suffixed_name = f"{p.name}__{p.location}"
|
|
345
345
|
param_value = None
|
|
346
346
|
|
|
347
|
+
suffixed_value = arguments.get(suffixed_name)
|
|
347
348
|
if (
|
|
348
349
|
suffixed_name in arguments
|
|
349
|
-
and
|
|
350
|
-
and
|
|
350
|
+
and suffixed_value is not None
|
|
351
|
+
and suffixed_value != ""
|
|
352
|
+
and not (
|
|
353
|
+
isinstance(suffixed_value, list | dict)
|
|
354
|
+
and len(suffixed_value) == 0
|
|
355
|
+
)
|
|
351
356
|
):
|
|
352
357
|
param_value = arguments[suffixed_name]
|
|
353
|
-
|
|
354
|
-
p.name
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
358
|
+
else:
|
|
359
|
+
name_value = arguments.get(p.name)
|
|
360
|
+
if (
|
|
361
|
+
p.name in arguments
|
|
362
|
+
and name_value is not None
|
|
363
|
+
and name_value != ""
|
|
364
|
+
and not (
|
|
365
|
+
isinstance(name_value, list | dict) and len(name_value) == 0
|
|
366
|
+
)
|
|
367
|
+
):
|
|
368
|
+
param_value = arguments[p.name]
|
|
359
369
|
|
|
360
370
|
if param_value is not None:
|
|
361
371
|
# Handle different parameter styles and types
|
|
@@ -367,7 +377,11 @@ class OpenAPITool(Tool):
|
|
|
367
377
|
) # Default explode for query is True
|
|
368
378
|
|
|
369
379
|
# Handle deepObject style for object parameters
|
|
370
|
-
if
|
|
380
|
+
if (
|
|
381
|
+
param_style == "deepObject"
|
|
382
|
+
and isinstance(param_value, dict)
|
|
383
|
+
and len(param_value) > 0
|
|
384
|
+
):
|
|
371
385
|
if param_explode:
|
|
372
386
|
# deepObject with explode=true: object properties become separate parameters
|
|
373
387
|
# e.g., target[id]=123&target[type]=user
|
|
@@ -386,6 +400,7 @@ class OpenAPITool(Tool):
|
|
|
386
400
|
elif (
|
|
387
401
|
isinstance(param_value, list)
|
|
388
402
|
and p.schema_.get("type") == "array"
|
|
403
|
+
and len(param_value) > 0
|
|
389
404
|
):
|
|
390
405
|
if param_explode:
|
|
391
406
|
# When explode=True, we pass the array directly, which HTTPX will serialize
|
|
@@ -444,9 +459,7 @@ class OpenAPITool(Tool):
|
|
|
444
459
|
params_to_exclude.add(p.name)
|
|
445
460
|
|
|
446
461
|
body_params = {
|
|
447
|
-
k: v
|
|
448
|
-
for k, v in arguments.items()
|
|
449
|
-
if k not in params_to_exclude and k != "context"
|
|
462
|
+
k: v for k, v in arguments.items() if k not in params_to_exclude
|
|
450
463
|
}
|
|
451
464
|
|
|
452
465
|
if body_params:
|
|
@@ -879,7 +892,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
879
892
|
|
|
880
893
|
# Extract output schema from OpenAPI responses
|
|
881
894
|
output_schema = extract_output_schema_from_responses(
|
|
882
|
-
route.responses, route.schema_definitions
|
|
895
|
+
route.responses, route.schema_definitions, route.openapi_version
|
|
883
896
|
)
|
|
884
897
|
|
|
885
898
|
# Get a unique tool name
|
fastmcp/server/proxy.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
7
7
|
from urllib.parse import quote
|
|
8
8
|
|
|
9
9
|
import mcp.types
|
|
10
|
+
from mcp import ServerSession
|
|
10
11
|
from mcp.client.session import ClientSession
|
|
11
12
|
from mcp.shared.context import LifespanContextT, RequestContext
|
|
12
13
|
from mcp.shared.exceptions import McpError
|
|
@@ -36,6 +37,9 @@ from fastmcp.server.dependencies import get_context
|
|
|
36
37
|
from fastmcp.server.server import FastMCP
|
|
37
38
|
from fastmcp.tools.tool import Tool, ToolResult
|
|
38
39
|
from fastmcp.tools.tool_manager import ToolManager
|
|
40
|
+
from fastmcp.tools.tool_transform import (
|
|
41
|
+
apply_transformations_to_tools,
|
|
42
|
+
)
|
|
39
43
|
from fastmcp.utilities.components import MirroredComponent
|
|
40
44
|
from fastmcp.utilities.logging import get_logger
|
|
41
45
|
|
|
@@ -71,7 +75,12 @@ class ProxyToolManager(ToolManager):
|
|
|
71
75
|
else:
|
|
72
76
|
raise e
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
transformed_tools = apply_transformations_to_tools(
|
|
79
|
+
tools=all_tools,
|
|
80
|
+
transformations=self.transformations,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return transformed_tools
|
|
75
84
|
|
|
76
85
|
async def list_tools(self) -> list[Tool]:
|
|
77
86
|
"""Gets the filtered list of tools including local, mounted, and proxy tools."""
|
|
@@ -246,6 +255,8 @@ class ProxyTool(Tool, MirroredComponent):
|
|
|
246
255
|
parameters=mcp_tool.inputSchema,
|
|
247
256
|
annotations=mcp_tool.annotations,
|
|
248
257
|
output_schema=mcp_tool.outputSchema,
|
|
258
|
+
meta=mcp_tool.meta,
|
|
259
|
+
tags=(mcp_tool.meta or {}).get("_fastmcp", {}).get("tags", []),
|
|
249
260
|
_mirrored=True,
|
|
250
261
|
)
|
|
251
262
|
|
|
@@ -294,12 +305,15 @@ class ProxyResource(Resource, MirroredComponent):
|
|
|
294
305
|
mcp_resource: mcp.types.Resource,
|
|
295
306
|
) -> ProxyResource:
|
|
296
307
|
"""Factory method to create a ProxyResource from a raw MCP resource schema."""
|
|
308
|
+
|
|
297
309
|
return cls(
|
|
298
310
|
client=client,
|
|
299
311
|
uri=mcp_resource.uri,
|
|
300
312
|
name=mcp_resource.name,
|
|
301
313
|
description=mcp_resource.description,
|
|
302
314
|
mime_type=mcp_resource.mimeType or "text/plain",
|
|
315
|
+
meta=mcp_resource.meta,
|
|
316
|
+
tags=(mcp_resource.meta or {}).get("_fastmcp", {}).get("tags", []),
|
|
303
317
|
_mirrored=True,
|
|
304
318
|
)
|
|
305
319
|
|
|
@@ -339,6 +353,8 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent):
|
|
|
339
353
|
description=mcp_template.description,
|
|
340
354
|
mime_type=mcp_template.mimeType or "text/plain",
|
|
341
355
|
parameters={}, # Remote templates don't have local parameters
|
|
356
|
+
meta=mcp_template.meta,
|
|
357
|
+
tags=(mcp_template.meta or {}).get("_fastmcp", {}).get("tags", []),
|
|
342
358
|
_mirrored=True,
|
|
343
359
|
)
|
|
344
360
|
|
|
@@ -371,6 +387,8 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent):
|
|
|
371
387
|
name=self.name,
|
|
372
388
|
description=self.description,
|
|
373
389
|
mime_type=result[0].mimeType,
|
|
390
|
+
meta=self.meta,
|
|
391
|
+
tags=(self.meta or {}).get("_fastmcp", {}).get("tags", []),
|
|
374
392
|
_value=value,
|
|
375
393
|
)
|
|
376
394
|
|
|
@@ -404,6 +422,8 @@ class ProxyPrompt(Prompt, MirroredComponent):
|
|
|
404
422
|
name=mcp_prompt.name,
|
|
405
423
|
description=mcp_prompt.description,
|
|
406
424
|
arguments=arguments,
|
|
425
|
+
meta=mcp_prompt.meta,
|
|
426
|
+
tags=(mcp_prompt.meta or {}).get("_fastmcp", {}).get("tags", []),
|
|
407
427
|
_mirrored=True,
|
|
408
428
|
)
|
|
409
429
|
|
|
@@ -469,7 +489,11 @@ class FastMCPProxy(FastMCP):
|
|
|
469
489
|
raise ValueError("Must specify 'client_factory'")
|
|
470
490
|
|
|
471
491
|
# Replace the default managers with our specialized proxy managers.
|
|
472
|
-
self._tool_manager = ProxyToolManager(
|
|
492
|
+
self._tool_manager = ProxyToolManager(
|
|
493
|
+
client_factory=self.client_factory,
|
|
494
|
+
# Propagate the transformations from the base class tool manager
|
|
495
|
+
transformations=self._tool_manager.transformations,
|
|
496
|
+
)
|
|
473
497
|
self._resource_manager = ProxyResourceManager(
|
|
474
498
|
client_factory=self.client_factory
|
|
475
499
|
)
|
|
@@ -554,11 +578,12 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
554
578
|
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.
|
|
555
579
|
"""
|
|
556
580
|
ctx = get_context()
|
|
557
|
-
result = await ctx.elicit(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
581
|
+
result = await ctx.session.elicit(
|
|
582
|
+
message=message,
|
|
583
|
+
requestedSchema=params.requestedSchema,
|
|
584
|
+
related_request_id=ctx.request_id,
|
|
585
|
+
)
|
|
586
|
+
return ElicitResult(action=result.action, content=result.content)
|
|
562
587
|
|
|
563
588
|
@classmethod
|
|
564
589
|
async def default_log_handler(cls, message: LogMessage) -> None:
|
|
@@ -566,7 +591,9 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
566
591
|
A handler that forwards the log notification from the remote server to the proxy's connected clients.
|
|
567
592
|
"""
|
|
568
593
|
ctx = get_context()
|
|
569
|
-
|
|
594
|
+
msg = message.data.get("msg")
|
|
595
|
+
extra = message.data.get("extra")
|
|
596
|
+
await ctx.log(msg, level=message.level, logger_name=message.logger, extra=extra)
|
|
570
597
|
|
|
571
598
|
@classmethod
|
|
572
599
|
async def default_progress_handler(
|
|
@@ -580,3 +607,57 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
580
607
|
"""
|
|
581
608
|
ctx = get_context()
|
|
582
609
|
await ctx.report_progress(progress, total, message)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class StatefulProxyClient(ProxyClient[ClientTransportT]):
|
|
613
|
+
"""
|
|
614
|
+
A proxy client that provides a stateful client factory for the proxy server.
|
|
615
|
+
|
|
616
|
+
The stateful proxy client bound its copy to the server session.
|
|
617
|
+
And it will be disconnected when the session is exited.
|
|
618
|
+
|
|
619
|
+
This is useful to proxy a stateful mcp server such as the Playwright MCP server.
|
|
620
|
+
Note that it is essential to ensure that the proxy server itself is also stateful.
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
def __init__(self, *args, **kwargs):
|
|
624
|
+
super().__init__(*args, **kwargs)
|
|
625
|
+
self._caches: dict[ServerSession, Client[ClientTransportT]] = {}
|
|
626
|
+
|
|
627
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
|
|
628
|
+
"""
|
|
629
|
+
The stateful proxy client will be forced disconnected when the session is exited.
|
|
630
|
+
So we do nothing here.
|
|
631
|
+
"""
|
|
632
|
+
pass
|
|
633
|
+
|
|
634
|
+
async def clear(self):
|
|
635
|
+
"""
|
|
636
|
+
Clear all cached clients and force disconnect them.
|
|
637
|
+
"""
|
|
638
|
+
while self._caches:
|
|
639
|
+
_, cache = self._caches.popitem()
|
|
640
|
+
await cache._disconnect(force=True)
|
|
641
|
+
|
|
642
|
+
def new_stateful(self) -> Client[ClientTransportT]:
|
|
643
|
+
"""
|
|
644
|
+
Create a new stateful proxy client instance with the same configuration.
|
|
645
|
+
|
|
646
|
+
Use this method as the client factory for stateful proxy server.
|
|
647
|
+
"""
|
|
648
|
+
session = get_context().session
|
|
649
|
+
proxy_client = self._caches.get(session, None)
|
|
650
|
+
|
|
651
|
+
if proxy_client is None:
|
|
652
|
+
proxy_client = self.new()
|
|
653
|
+
logger.debug(f"{proxy_client} created for {session}")
|
|
654
|
+
self._caches[session] = proxy_client
|
|
655
|
+
|
|
656
|
+
async def _on_session_exit():
|
|
657
|
+
self._caches.pop(session)
|
|
658
|
+
logger.debug(f"{proxy_client} will be disconnect")
|
|
659
|
+
await proxy_client._disconnect(force=True)
|
|
660
|
+
|
|
661
|
+
session._exit_stack.push_async_callback(_on_session_exit)
|
|
662
|
+
|
|
663
|
+
return proxy_client
|