fastmcp 2.13.0rc2__py3-none-any.whl → 2.13.0.1__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 +2 -2
- fastmcp/cli/cli.py +3 -2
- fastmcp/cli/install/claude_code.py +3 -3
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +7 -6
- fastmcp/client/client.py +10 -10
- fastmcp/client/oauth_callback.py +6 -2
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +35 -34
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +1 -1
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +2 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +3 -5
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +2 -3
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +9 -13
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +1 -3
- fastmcp/resources/resource_manager.py +1 -1
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +5 -5
- fastmcp/server/auth/auth.py +2 -2
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +39 -92
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +236 -217
- fastmcp/server/auth/oidc_proxy.py +18 -3
- fastmcp/server/auth/providers/auth0.py +28 -15
- fastmcp/server/auth/providers/aws.py +16 -1
- fastmcp/server/auth/providers/azure.py +101 -40
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/github.py +16 -1
- fastmcp/server/auth/providers/google.py +16 -1
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/auth/providers/introspection.py +2 -2
- fastmcp/server/auth/providers/jwt.py +17 -18
- fastmcp/server/auth/providers/supabase.py +1 -1
- fastmcp/server/auth/providers/workos.py +18 -3
- fastmcp/server/context.py +41 -12
- fastmcp/server/dependencies.py +5 -6
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +3 -4
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +1 -1
- fastmcp/server/middleware/error_handling.py +8 -8
- fastmcp/server/middleware/middleware.py +1 -1
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +5 -4
- fastmcp/server/server.py +74 -55
- fastmcp/settings.py +2 -1
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +12 -12
- fastmcp/tools/tool_manager.py +8 -4
- fastmcp/tools/tool_transform.py +6 -6
- fastmcp/utilities/cli.py +50 -21
- fastmcp/utilities/inspect.py +2 -2
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +14 -18
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +9 -9
- fastmcp/utilities/tests.py +2 -4
- fastmcp/utilities/ui.py +126 -6
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/METADATA +5 -5
- fastmcp-2.13.0.1.dist-info/RECORD +141 -0
- fastmcp-2.13.0rc2.dist-info/RECORD +0 -138
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -46,6 +46,7 @@ class WorkOSProviderSettings(BaseSettings):
|
|
|
46
46
|
required_scopes: list[str] | None = None
|
|
47
47
|
timeout_seconds: int | None = None
|
|
48
48
|
allowed_client_redirect_uris: list[str] | None = None
|
|
49
|
+
jwt_signing_key: str | None = None
|
|
49
50
|
|
|
50
51
|
@field_validator("required_scopes", mode="before")
|
|
51
52
|
@classmethod
|
|
@@ -168,10 +169,12 @@ class WorkOSProvider(OAuthProxy):
|
|
|
168
169
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
169
170
|
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
170
171
|
redirect_path: str | NotSetT = NotSet,
|
|
171
|
-
required_scopes: list[str] |
|
|
172
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
172
173
|
timeout_seconds: int | NotSetT = NotSet,
|
|
173
174
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
174
175
|
client_storage: AsyncKeyValue | None = None,
|
|
176
|
+
jwt_signing_key: str | bytes | NotSetT = NotSet,
|
|
177
|
+
require_authorization_consent: bool = True,
|
|
175
178
|
):
|
|
176
179
|
"""Initialize WorkOS OAuth provider.
|
|
177
180
|
|
|
@@ -187,7 +190,16 @@ class WorkOSProvider(OAuthProxy):
|
|
|
187
190
|
timeout_seconds: HTTP request timeout for WorkOS API calls
|
|
188
191
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
189
192
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
190
|
-
client_storage:
|
|
193
|
+
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
|
|
194
|
+
If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
|
|
195
|
+
disk store will be encrypted using a key derived from the JWT Signing Key.
|
|
196
|
+
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
|
|
197
|
+
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
|
|
198
|
+
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
|
|
199
|
+
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
|
200
|
+
When True, users see a consent screen before being redirected to WorkOS.
|
|
201
|
+
When False, authorization proceeds directly without user confirmation.
|
|
202
|
+
SECURITY WARNING: Only disable for local development or testing environments.
|
|
191
203
|
"""
|
|
192
204
|
|
|
193
205
|
settings = WorkOSProviderSettings.model_validate(
|
|
@@ -203,6 +215,7 @@ class WorkOSProvider(OAuthProxy):
|
|
|
203
215
|
"required_scopes": required_scopes,
|
|
204
216
|
"timeout_seconds": timeout_seconds,
|
|
205
217
|
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
218
|
+
"jwt_signing_key": jwt_signing_key,
|
|
206
219
|
}.items()
|
|
207
220
|
if v is not NotSet
|
|
208
221
|
}
|
|
@@ -256,6 +269,8 @@ class WorkOSProvider(OAuthProxy):
|
|
|
256
269
|
or settings.base_url, # Default to base_url if not specified
|
|
257
270
|
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
258
271
|
client_storage=client_storage,
|
|
272
|
+
jwt_signing_key=settings.jwt_signing_key,
|
|
273
|
+
require_authorization_consent=require_authorization_consent,
|
|
259
274
|
)
|
|
260
275
|
|
|
261
276
|
logger.debug(
|
|
@@ -323,7 +338,7 @@ class AuthKitProvider(RemoteAuthProvider):
|
|
|
323
338
|
*,
|
|
324
339
|
authkit_domain: AnyHttpUrl | str | NotSetT = NotSet,
|
|
325
340
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
326
|
-
required_scopes: list[str] |
|
|
341
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
327
342
|
token_verifier: TokenVerifier | None = None,
|
|
328
343
|
):
|
|
329
344
|
"""Initialize AuthKit metadata provider.
|
fastmcp/server/context.py
CHANGED
|
@@ -22,6 +22,7 @@ from mcp.types import (
|
|
|
22
22
|
AudioContent,
|
|
23
23
|
ClientCapabilities,
|
|
24
24
|
CreateMessageResult,
|
|
25
|
+
GetPromptResult,
|
|
25
26
|
ImageContent,
|
|
26
27
|
IncludeContext,
|
|
27
28
|
ModelHint,
|
|
@@ -32,6 +33,8 @@ from mcp.types import (
|
|
|
32
33
|
TextContent,
|
|
33
34
|
)
|
|
34
35
|
from mcp.types import CreateMessageRequestParams as SamplingParams
|
|
36
|
+
from mcp.types import Prompt as MCPPrompt
|
|
37
|
+
from mcp.types import Resource as MCPResource
|
|
35
38
|
from pydantic.networks import AnyUrl
|
|
36
39
|
from starlette.requests import Request
|
|
37
40
|
from typing_extensions import TypeVar
|
|
@@ -185,8 +188,8 @@ class Context:
|
|
|
185
188
|
"""
|
|
186
189
|
try:
|
|
187
190
|
return request_ctx.get()
|
|
188
|
-
except LookupError:
|
|
189
|
-
raise ValueError("Context is not available outside of a request")
|
|
191
|
+
except LookupError as e:
|
|
192
|
+
raise ValueError("Context is not available outside of a request") from e
|
|
190
193
|
|
|
191
194
|
async def report_progress(
|
|
192
195
|
self, progress: float, total: float | None = None, message: str | None = None
|
|
@@ -215,6 +218,36 @@ class Context:
|
|
|
215
218
|
related_request_id=self.request_id,
|
|
216
219
|
)
|
|
217
220
|
|
|
221
|
+
async def list_resources(self) -> list[MCPResource]:
|
|
222
|
+
"""List all available resources from the server.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of Resource objects available on the server
|
|
226
|
+
"""
|
|
227
|
+
return await self.fastmcp._list_resources_mcp()
|
|
228
|
+
|
|
229
|
+
async def list_prompts(self) -> list[MCPPrompt]:
|
|
230
|
+
"""List all available prompts from the server.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of Prompt objects available on the server
|
|
234
|
+
"""
|
|
235
|
+
return await self.fastmcp._list_prompts_mcp()
|
|
236
|
+
|
|
237
|
+
async def get_prompt(
|
|
238
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
239
|
+
) -> GetPromptResult:
|
|
240
|
+
"""Get a prompt by name with optional arguments.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
name: The name of the prompt to get
|
|
244
|
+
arguments: Optional arguments to pass to the prompt
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
The prompt result
|
|
248
|
+
"""
|
|
249
|
+
return await self.fastmcp._get_prompt_mcp(name, arguments)
|
|
250
|
+
|
|
218
251
|
async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
|
|
219
252
|
"""Read a resource by URI.
|
|
220
253
|
|
|
@@ -224,8 +257,6 @@ class Context:
|
|
|
224
257
|
Returns:
|
|
225
258
|
The resource content as either text or bytes
|
|
226
259
|
"""
|
|
227
|
-
if self.fastmcp is None:
|
|
228
|
-
raise ValueError("Context is not available outside of a request")
|
|
229
260
|
return await self.fastmcp._read_resource_mcp(uri)
|
|
230
261
|
|
|
231
262
|
async def log(
|
|
@@ -311,7 +342,7 @@ class Context:
|
|
|
311
342
|
session_id = str(uuid4())
|
|
312
343
|
|
|
313
344
|
# Save the session id to the session attributes
|
|
314
|
-
|
|
345
|
+
session._fastmcp_id = session_id
|
|
315
346
|
return session_id
|
|
316
347
|
|
|
317
348
|
@property
|
|
@@ -564,13 +595,11 @@ class Context:
|
|
|
564
595
|
choice_literal = Literal[tuple(response_type)] # type: ignore
|
|
565
596
|
response_type = ScalarElicitationType[choice_literal] # type: ignore
|
|
566
597
|
# if the user provided a primitive scalar, wrap it in an object schema
|
|
567
|
-
elif
|
|
568
|
-
response_type
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
# if the user provided an Enum type, wrap it in an object schema
|
|
573
|
-
elif isinstance(response_type, type) and issubclass(response_type, Enum):
|
|
598
|
+
elif (
|
|
599
|
+
response_type in {bool, int, float, str}
|
|
600
|
+
or get_origin(response_type) is Literal
|
|
601
|
+
or (isinstance(response_type, type) and issubclass(response_type, Enum))
|
|
602
|
+
):
|
|
574
603
|
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
575
604
|
|
|
576
605
|
response_type = cast(type[T], response_type)
|
fastmcp/server/dependencies.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from mcp.server.auth.middleware.auth_context import (
|
|
@@ -16,11 +17,11 @@ if TYPE_CHECKING:
|
|
|
16
17
|
from fastmcp.server.context import Context
|
|
17
18
|
|
|
18
19
|
__all__ = [
|
|
20
|
+
"AccessToken",
|
|
21
|
+
"get_access_token",
|
|
19
22
|
"get_context",
|
|
20
|
-
"get_http_request",
|
|
21
23
|
"get_http_headers",
|
|
22
|
-
"
|
|
23
|
-
"AccessToken",
|
|
24
|
+
"get_http_request",
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
|
|
@@ -43,10 +44,8 @@ def get_http_request() -> Request:
|
|
|
43
44
|
from mcp.server.lowlevel.server import request_ctx
|
|
44
45
|
|
|
45
46
|
request = None
|
|
46
|
-
|
|
47
|
+
with contextlib.suppress(LookupError):
|
|
47
48
|
request = request_ctx.get().request
|
|
48
|
-
except LookupError:
|
|
49
|
-
pass
|
|
50
49
|
|
|
51
50
|
if request is None:
|
|
52
51
|
raise RuntimeError("No active HTTP request found.")
|
fastmcp/server/elicitation.py
CHANGED
fastmcp/server/http.py
CHANGED
|
@@ -5,7 +5,6 @@ from contextlib import asynccontextmanager, contextmanager
|
|
|
5
5
|
from contextvars import ContextVar
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware
|
|
9
8
|
from mcp.server.auth.routes import build_resource_metadata_url
|
|
10
9
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
11
10
|
from mcp.server.sse import SseServerTransport
|
|
@@ -21,6 +20,7 @@ from starlette.routing import BaseRoute, Mount, Route
|
|
|
21
20
|
from starlette.types import Lifespan, Receive, Scope, Send
|
|
22
21
|
|
|
23
22
|
from fastmcp.server.auth import AuthProvider
|
|
23
|
+
from fastmcp.server.auth.middleware import RequireAuthMiddleware
|
|
24
24
|
from fastmcp.utilities.logging import get_logger
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
@@ -342,9 +342,8 @@ def create_streamable_http_app(
|
|
|
342
342
|
# Create a lifespan manager to start and stop the session manager
|
|
343
343
|
@asynccontextmanager
|
|
344
344
|
async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
|
|
345
|
-
async with server._lifespan_manager():
|
|
346
|
-
|
|
347
|
-
yield
|
|
345
|
+
async with server._lifespan_manager(), session_manager.run():
|
|
346
|
+
yield
|
|
348
347
|
|
|
349
348
|
# Create and return the app with lifespan
|
|
350
349
|
app = create_base_app(
|
|
@@ -46,7 +46,7 @@ class CachableReadResourceContents(BaseModel):
|
|
|
46
46
|
|
|
47
47
|
@classmethod
|
|
48
48
|
def get_sizes(cls, values: Sequence[Self]) -> int:
|
|
49
|
-
return sum(
|
|
49
|
+
return sum(item.get_size() for item in values)
|
|
50
50
|
|
|
51
51
|
@classmethod
|
|
52
52
|
def wrap(cls, values: Sequence[ReadResourceContents]) -> list[Self]:
|
|
@@ -64,7 +64,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
64
64
|
error_key = f"{error_type}:{method}"
|
|
65
65
|
self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1
|
|
66
66
|
|
|
67
|
-
base_message = f"Error in {method}: {error_type}: {
|
|
67
|
+
base_message = f"Error in {method}: {error_type}: {error!s}"
|
|
68
68
|
|
|
69
69
|
if self.include_traceback:
|
|
70
70
|
self.logger.error(f"{base_message}\n{traceback.format_exc()}")
|
|
@@ -91,24 +91,24 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
91
91
|
|
|
92
92
|
if error_type in (ValueError, TypeError):
|
|
93
93
|
return McpError(
|
|
94
|
-
ErrorData(code=-32602, message=f"Invalid params: {
|
|
94
|
+
ErrorData(code=-32602, message=f"Invalid params: {error!s}")
|
|
95
95
|
)
|
|
96
96
|
elif error_type in (FileNotFoundError, KeyError, NotFoundError):
|
|
97
97
|
return McpError(
|
|
98
|
-
ErrorData(code=-32001, message=f"Resource not found: {
|
|
98
|
+
ErrorData(code=-32001, message=f"Resource not found: {error!s}")
|
|
99
99
|
)
|
|
100
100
|
elif error_type is PermissionError:
|
|
101
101
|
return McpError(
|
|
102
|
-
ErrorData(code=-32000, message=f"Permission denied: {
|
|
102
|
+
ErrorData(code=-32000, message=f"Permission denied: {error!s}")
|
|
103
103
|
)
|
|
104
104
|
# asyncio.TimeoutError is a subclass of TimeoutError in Python 3.10, alias in 3.11+
|
|
105
105
|
elif error_type in (TimeoutError, asyncio.TimeoutError):
|
|
106
106
|
return McpError(
|
|
107
|
-
ErrorData(code=-32000, message=f"Request timeout: {
|
|
107
|
+
ErrorData(code=-32000, message=f"Request timeout: {error!s}")
|
|
108
108
|
)
|
|
109
109
|
else:
|
|
110
110
|
return McpError(
|
|
111
|
-
ErrorData(code=-32603, message=f"Internal error: {
|
|
111
|
+
ErrorData(code=-32603, message=f"Internal error: {error!s}")
|
|
112
112
|
)
|
|
113
113
|
|
|
114
114
|
async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
|
|
@@ -120,7 +120,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
120
120
|
|
|
121
121
|
# Transform and re-raise
|
|
122
122
|
transformed_error = self._transform_error(error)
|
|
123
|
-
raise transformed_error
|
|
123
|
+
raise transformed_error from error
|
|
124
124
|
|
|
125
125
|
def get_error_stats(self) -> dict[str, int]:
|
|
126
126
|
"""Get error statistics for monitoring."""
|
|
@@ -200,7 +200,7 @@ class RetryMiddleware(Middleware):
|
|
|
200
200
|
delay = self._calculate_delay(attempt)
|
|
201
201
|
self.logger.warning(
|
|
202
202
|
f"Request {context.method} failed (attempt {attempt + 1}/{self.max_retries + 1}): "
|
|
203
|
-
f"{type(error).__name__}: {
|
|
203
|
+
f"{type(error).__name__}: {error!s}. Retrying in {delay:.1f}s..."
|
|
204
204
|
)
|
|
205
205
|
|
|
206
206
|
await anyio.sleep(delay)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""A middleware for injecting tools into the MCP server context."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
import mcp.types
|
|
8
|
+
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
9
|
+
from mcp.types import Prompt
|
|
10
|
+
from pydantic import AnyUrl
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from fastmcp.server.context import Context
|
|
14
|
+
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
|
|
15
|
+
from fastmcp.tools.tool import Tool, ToolResult
|
|
16
|
+
from fastmcp.utilities.logging import get_logger
|
|
17
|
+
|
|
18
|
+
logger: Logger = get_logger(name=__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolInjectionMiddleware(Middleware):
|
|
22
|
+
"""A middleware for injecting tools into the context."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, tools: Sequence[Tool]):
|
|
25
|
+
"""Initialize the tool injection middleware."""
|
|
26
|
+
self._tools_to_inject: Sequence[Tool] = tools
|
|
27
|
+
self._tools_to_inject_by_name: dict[str, Tool] = {
|
|
28
|
+
tool.name: tool for tool in tools
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
async def on_list_tools(
|
|
33
|
+
self,
|
|
34
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
35
|
+
call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]],
|
|
36
|
+
) -> Sequence[Tool]:
|
|
37
|
+
"""Inject tools into the response."""
|
|
38
|
+
return [*self._tools_to_inject, *await call_next(context)]
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
async def on_call_tool(
|
|
42
|
+
self,
|
|
43
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
44
|
+
call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],
|
|
45
|
+
) -> ToolResult:
|
|
46
|
+
"""Intercept tool calls to injected tools."""
|
|
47
|
+
if context.message.name in self._tools_to_inject_by_name:
|
|
48
|
+
tool = self._tools_to_inject_by_name[context.message.name]
|
|
49
|
+
return await tool.run(arguments=context.message.arguments or {})
|
|
50
|
+
|
|
51
|
+
return await call_next(context)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def list_prompts(context: Context) -> list[Prompt]:
|
|
55
|
+
"""List prompts available on the server."""
|
|
56
|
+
return await context.list_prompts()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
list_prompts_tool = Tool.from_function(
|
|
60
|
+
fn=list_prompts,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def get_prompt(
|
|
65
|
+
context: Context,
|
|
66
|
+
name: Annotated[str, "The name of the prompt to render."],
|
|
67
|
+
arguments: Annotated[
|
|
68
|
+
dict[str, Any] | None, "The arguments to pass to the prompt."
|
|
69
|
+
] = None,
|
|
70
|
+
) -> mcp.types.GetPromptResult:
|
|
71
|
+
"""Render a prompt available on the server."""
|
|
72
|
+
return await context.get_prompt(name=name, arguments=arguments)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
get_prompt_tool = Tool.from_function(
|
|
76
|
+
fn=get_prompt,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PromptToolMiddleware(ToolInjectionMiddleware):
|
|
81
|
+
"""A middleware for injecting prompts as tools into the context."""
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
tools: list[Tool] = [list_prompts_tool, get_prompt_tool]
|
|
85
|
+
super().__init__(tools=tools)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def list_resources(context: Context) -> list[mcp.types.Resource]:
|
|
89
|
+
"""List resources available on the server."""
|
|
90
|
+
return await context.list_resources()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
list_resources_tool = Tool.from_function(
|
|
94
|
+
fn=list_resources,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def read_resource(
|
|
99
|
+
context: Context,
|
|
100
|
+
uri: Annotated[AnyUrl | str, "The URI of the resource to read."],
|
|
101
|
+
) -> list[ReadResourceContents]:
|
|
102
|
+
"""Read a resource available on the server."""
|
|
103
|
+
return await context.read_resource(uri=uri)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
read_resource_tool = Tool.from_function(
|
|
107
|
+
fn=read_resource,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ResourceToolMiddleware(ToolInjectionMiddleware):
|
|
112
|
+
"""A middleware for injecting resources as tools into the context."""
|
|
113
|
+
|
|
114
|
+
def __init__(self) -> None:
|
|
115
|
+
tools: list[Tool] = [list_resources_tool, read_resource_tool]
|
|
116
|
+
super().__init__(tools=tools)
|
fastmcp/server/openapi.py
CHANGED
|
@@ -513,11 +513,11 @@ class OpenAPITool(Tool):
|
|
|
513
513
|
if e.response.text:
|
|
514
514
|
error_message += f" - {e.response.text}"
|
|
515
515
|
|
|
516
|
-
raise ValueError(error_message)
|
|
516
|
+
raise ValueError(error_message) from e
|
|
517
517
|
|
|
518
518
|
except httpx.RequestError as e:
|
|
519
519
|
# Handle request errors (connection, timeout, etc.)
|
|
520
|
-
raise ValueError(f"Request error: {
|
|
520
|
+
raise ValueError(f"Request error: {e!s}") from e
|
|
521
521
|
|
|
522
522
|
|
|
523
523
|
class OpenAPIResource(Resource):
|
|
@@ -531,9 +531,11 @@ class OpenAPIResource(Resource):
|
|
|
531
531
|
name: str,
|
|
532
532
|
description: str,
|
|
533
533
|
mime_type: str = "application/json",
|
|
534
|
-
tags: set[str] =
|
|
534
|
+
tags: set[str] | None = None,
|
|
535
535
|
timeout: float | None = None,
|
|
536
536
|
):
|
|
537
|
+
if tags is None:
|
|
538
|
+
tags = set()
|
|
537
539
|
super().__init__(
|
|
538
540
|
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
539
541
|
name=name,
|
|
@@ -632,11 +634,11 @@ class OpenAPIResource(Resource):
|
|
|
632
634
|
if e.response.text:
|
|
633
635
|
error_message += f" - {e.response.text}"
|
|
634
636
|
|
|
635
|
-
raise ValueError(error_message)
|
|
637
|
+
raise ValueError(error_message) from e
|
|
636
638
|
|
|
637
639
|
except httpx.RequestError as e:
|
|
638
640
|
# Handle request errors (connection, timeout, etc.)
|
|
639
|
-
raise ValueError(f"Request error: {
|
|
641
|
+
raise ValueError(f"Request error: {e!s}") from e
|
|
640
642
|
|
|
641
643
|
|
|
642
644
|
class OpenAPIResourceTemplate(ResourceTemplate):
|
|
@@ -650,9 +652,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
650
652
|
name: str,
|
|
651
653
|
description: str,
|
|
652
654
|
parameters: dict[str, Any],
|
|
653
|
-
tags: set[str] =
|
|
655
|
+
tags: set[str] | None = None,
|
|
654
656
|
timeout: float | None = None,
|
|
655
657
|
):
|
|
658
|
+
if tags is None:
|
|
659
|
+
tags = set()
|
|
656
660
|
super().__init__(
|
|
657
661
|
uri_template=uri_template,
|
|
658
662
|
name=name,
|
fastmcp/server/proxy.py
CHANGED
|
@@ -198,7 +198,9 @@ class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
|
|
|
198
198
|
elif isinstance(result[0], BlobResourceContents):
|
|
199
199
|
return result[0].blob
|
|
200
200
|
else:
|
|
201
|
-
raise ResourceError(
|
|
201
|
+
raise ResourceError(
|
|
202
|
+
f"Unsupported content type: {type(result[0])}"
|
|
203
|
+
) from None
|
|
202
204
|
|
|
203
205
|
|
|
204
206
|
class ProxyPromptManager(PromptManager, ProxyManagerMixin):
|
|
@@ -558,7 +560,7 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
558
560
|
kwargs["log_handler"] = ProxyClient.default_log_handler
|
|
559
561
|
if "progress_handler" not in kwargs:
|
|
560
562
|
kwargs["progress_handler"] = ProxyClient.default_progress_handler
|
|
561
|
-
super().__init__(**kwargs |
|
|
563
|
+
super().__init__(**kwargs | {"transport": transport})
|
|
562
564
|
|
|
563
565
|
@classmethod
|
|
564
566
|
async def default_sampling_handler(
|
|
@@ -572,7 +574,7 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
572
574
|
"""
|
|
573
575
|
ctx = get_context()
|
|
574
576
|
content = await ctx.sample(
|
|
575
|
-
|
|
577
|
+
list(messages),
|
|
576
578
|
system_prompt=params.systemPrompt,
|
|
577
579
|
temperature=params.temperature,
|
|
578
580
|
max_tokens=params.maxTokens,
|
|
@@ -649,7 +651,6 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
|
|
|
649
651
|
The stateful proxy client will be forced disconnected when the session is exited.
|
|
650
652
|
So we do nothing here.
|
|
651
653
|
"""
|
|
652
|
-
pass
|
|
653
654
|
|
|
654
655
|
async def clear(self):
|
|
655
656
|
"""
|