fastmcp 2.11.2__py3-none-any.whl → 2.12.0rc1__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 +5 -4
- fastmcp/cli/claude.py +22 -18
- fastmcp/cli/cli.py +472 -136
- fastmcp/cli/install/claude_code.py +37 -40
- fastmcp/cli/install/claude_desktop.py +37 -42
- fastmcp/cli/install/cursor.py +148 -38
- fastmcp/cli/install/mcp_json.py +38 -43
- fastmcp/cli/install/shared.py +64 -7
- fastmcp/cli/run.py +122 -215
- fastmcp/client/auth/oauth.py +69 -13
- fastmcp/client/client.py +46 -9
- fastmcp/client/logging.py +25 -1
- fastmcp/client/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +143 -67
- fastmcp/experimental/sampling/__init__.py +0 -0
- fastmcp/experimental/sampling/handlers/__init__.py +3 -0
- fastmcp/experimental/sampling/handlers/base.py +21 -0
- fastmcp/experimental/sampling/handlers/openai.py +163 -0
- fastmcp/experimental/server/openapi/routing.py +1 -3
- fastmcp/experimental/server/openapi/server.py +10 -25
- fastmcp/experimental/utilities/openapi/__init__.py +2 -2
- fastmcp/experimental/utilities/openapi/formatters.py +34 -0
- fastmcp/experimental/utilities/openapi/models.py +5 -2
- fastmcp/experimental/utilities/openapi/parser.py +252 -70
- fastmcp/experimental/utilities/openapi/schemas.py +135 -106
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +4 -2
- fastmcp/resources/resource_manager.py +16 -6
- fastmcp/server/auth/__init__.py +11 -1
- fastmcp/server/auth/auth.py +19 -2
- fastmcp/server/auth/oauth_proxy.py +1047 -0
- fastmcp/server/auth/providers/azure.py +270 -0
- fastmcp/server/auth/providers/github.py +287 -0
- fastmcp/server/auth/providers/google.py +305 -0
- fastmcp/server/auth/providers/jwt.py +27 -16
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/auth/registry.py +1 -1
- fastmcp/server/context.py +91 -41
- fastmcp/server/dependencies.py +32 -2
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +44 -37
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +85 -20
- fastmcp/settings.py +18 -3
- fastmcp/tools/tool.py +23 -10
- fastmcp/tools/tool_manager.py +5 -1
- fastmcp/tools/tool_transform.py +75 -32
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +21 -5
- fastmcp/utilities/inspect.py +166 -37
- fastmcp/utilities/json_schema_type.py +4 -2
- fastmcp/utilities/logging.py +4 -1
- fastmcp/utilities/mcp_config.py +47 -18
- fastmcp/utilities/mcp_server_config/__init__.py +25 -0
- fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
- fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
- fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
- fastmcp/utilities/openapi.py +4 -4
- fastmcp/utilities/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/METADATA +3 -2
- fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
- fastmcp-2.11.2.dist-info/RECORD +0 -108
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from logging import Logger
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
9
|
+
import pydantic_core
|
|
10
|
+
|
|
7
11
|
from .middleware import CallNext, Middleware, MiddlewareContext
|
|
8
12
|
|
|
9
13
|
|
|
14
|
+
def default_serializer(data: Any) -> str:
|
|
15
|
+
"""The default serializer for Payloads in the logging middleware."""
|
|
16
|
+
return pydantic_core.to_json(data, fallback=str).decode()
|
|
17
|
+
|
|
18
|
+
|
|
10
19
|
class LoggingMiddleware(Middleware):
|
|
11
20
|
"""Middleware that provides comprehensive request and response logging.
|
|
12
21
|
|
|
@@ -33,6 +42,7 @@ class LoggingMiddleware(Middleware):
|
|
|
33
42
|
include_payloads: bool = False,
|
|
34
43
|
max_payload_length: int = 1000,
|
|
35
44
|
methods: list[str] | None = None,
|
|
45
|
+
payload_serializer: Callable[[Any], str] | None = None,
|
|
36
46
|
):
|
|
37
47
|
"""Initialize logging middleware.
|
|
38
48
|
|
|
@@ -43,13 +53,14 @@ class LoggingMiddleware(Middleware):
|
|
|
43
53
|
max_payload_length: Maximum length of payload to log (prevents huge logs)
|
|
44
54
|
methods: List of methods to log. If None, logs all methods.
|
|
45
55
|
"""
|
|
46
|
-
self.logger = logger or logging.getLogger("fastmcp.requests")
|
|
47
|
-
self.log_level = log_level
|
|
48
|
-
self.include_payloads = include_payloads
|
|
49
|
-
self.max_payload_length = max_payload_length
|
|
50
|
-
self.methods = methods
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
self.logger: Logger = logger or logging.getLogger("fastmcp.requests")
|
|
57
|
+
self.log_level: int = log_level
|
|
58
|
+
self.include_payloads: bool = include_payloads
|
|
59
|
+
self.max_payload_length: int = max_payload_length
|
|
60
|
+
self.methods: list[str] | None = methods
|
|
61
|
+
self.payload_serializer: Callable[[Any], str] | None = payload_serializer
|
|
62
|
+
|
|
63
|
+
def _format_message(self, context: MiddlewareContext[Any]) -> str:
|
|
53
64
|
"""Format a message for logging."""
|
|
54
65
|
parts = [
|
|
55
66
|
f"source={context.source}",
|
|
@@ -57,18 +68,29 @@ class LoggingMiddleware(Middleware):
|
|
|
57
68
|
f"method={context.method or 'unknown'}",
|
|
58
69
|
]
|
|
59
70
|
|
|
60
|
-
if self.include_payloads
|
|
61
|
-
|
|
62
|
-
payload = json.dumps(context.message.__dict__, default=str)
|
|
63
|
-
if len(payload) > self.max_payload_length:
|
|
64
|
-
payload = payload[: self.max_payload_length] + "..."
|
|
65
|
-
parts.append(f"payload={payload}")
|
|
66
|
-
except (TypeError, ValueError):
|
|
67
|
-
parts.append("payload=<non-serializable>")
|
|
71
|
+
if self.include_payloads:
|
|
72
|
+
payload: str
|
|
68
73
|
|
|
74
|
+
if not self.payload_serializer:
|
|
75
|
+
payload = default_serializer(context.message)
|
|
76
|
+
else:
|
|
77
|
+
try:
|
|
78
|
+
payload = self.payload_serializer(context.message)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.logger.warning(
|
|
81
|
+
f"Failed {e} to serialize payload: {context.type} {context.method} {context.source}."
|
|
82
|
+
)
|
|
83
|
+
payload = default_serializer(context.message)
|
|
84
|
+
|
|
85
|
+
if len(payload) > self.max_payload_length:
|
|
86
|
+
payload = payload[: self.max_payload_length] + "..."
|
|
87
|
+
|
|
88
|
+
parts.append(f"payload={payload}")
|
|
69
89
|
return " ".join(parts)
|
|
70
90
|
|
|
71
|
-
async def on_message(
|
|
91
|
+
async def on_message(
|
|
92
|
+
self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
|
|
93
|
+
) -> Any:
|
|
72
94
|
"""Log all messages."""
|
|
73
95
|
message_info = self._format_message(context)
|
|
74
96
|
if self.methods and context.method not in self.methods:
|
|
@@ -111,6 +133,7 @@ class StructuredLoggingMiddleware(Middleware):
|
|
|
111
133
|
log_level: int = logging.INFO,
|
|
112
134
|
include_payloads: bool = False,
|
|
113
135
|
methods: list[str] | None = None,
|
|
136
|
+
payload_serializer: Callable[[Any], str] | None = None,
|
|
114
137
|
):
|
|
115
138
|
"""Initialize structured logging middleware.
|
|
116
139
|
|
|
@@ -119,15 +142,18 @@ class StructuredLoggingMiddleware(Middleware):
|
|
|
119
142
|
log_level: Log level for messages (default: INFO)
|
|
120
143
|
include_payloads: Whether to include message payloads in logs
|
|
121
144
|
methods: List of methods to log. If None, logs all methods.
|
|
145
|
+
serializer: Callable that converts objects to a JSON string for the
|
|
146
|
+
payload. If not provided, uses FastMCP's default tool serializer.
|
|
122
147
|
"""
|
|
123
|
-
self.logger = logger or logging.getLogger("fastmcp.structured")
|
|
124
|
-
self.log_level = log_level
|
|
125
|
-
self.include_payloads = include_payloads
|
|
126
|
-
self.methods = methods
|
|
148
|
+
self.logger: Logger = logger or logging.getLogger("fastmcp.structured")
|
|
149
|
+
self.log_level: int = log_level
|
|
150
|
+
self.include_payloads: bool = include_payloads
|
|
151
|
+
self.methods: list[str] | None = methods
|
|
152
|
+
self.payload_serializer: Callable[[Any], str] | None = payload_serializer
|
|
127
153
|
|
|
128
154
|
def _create_log_entry(
|
|
129
|
-
self, context: MiddlewareContext, event: str, **extra_fields
|
|
130
|
-
) -> dict:
|
|
155
|
+
self, context: MiddlewareContext[Any], event: str, **extra_fields: Any
|
|
156
|
+
) -> dict[str, Any]:
|
|
131
157
|
"""Create a structured log entry."""
|
|
132
158
|
entry = {
|
|
133
159
|
"event": event,
|
|
@@ -138,15 +164,27 @@ class StructuredLoggingMiddleware(Middleware):
|
|
|
138
164
|
**extra_fields,
|
|
139
165
|
}
|
|
140
166
|
|
|
141
|
-
if self.include_payloads
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
if self.include_payloads:
|
|
168
|
+
payload: str
|
|
169
|
+
|
|
170
|
+
if not self.payload_serializer:
|
|
171
|
+
payload = default_serializer(context.message)
|
|
172
|
+
else:
|
|
173
|
+
try:
|
|
174
|
+
payload = self.payload_serializer(context.message)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self.logger.warning(
|
|
177
|
+
f"Failed {str(e)} to serialize payload: {context.type} {context.method} {context.source}."
|
|
178
|
+
)
|
|
179
|
+
payload = default_serializer(context.message)
|
|
180
|
+
|
|
181
|
+
entry["payload"] = payload
|
|
146
182
|
|
|
147
183
|
return entry
|
|
148
184
|
|
|
149
|
-
async def on_message(
|
|
185
|
+
async def on_message(
|
|
186
|
+
self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
|
|
187
|
+
) -> Any:
|
|
150
188
|
"""Log structured message information."""
|
|
151
189
|
start_entry = self._create_log_entry(context, "request_start")
|
|
152
190
|
if self.methods and context.method not in self.methods:
|
fastmcp/server/proxy.py
CHANGED
|
@@ -546,6 +546,8 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
546
546
|
| str,
|
|
547
547
|
**kwargs,
|
|
548
548
|
):
|
|
549
|
+
if "name" not in kwargs:
|
|
550
|
+
kwargs["name"] = self.generate_name()
|
|
549
551
|
if "roots" not in kwargs:
|
|
550
552
|
kwargs["roots"] = default_proxy_roots_handler
|
|
551
553
|
if "sampling_handler" not in kwargs:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from typing import TypeAlias
|
|
3
|
+
|
|
4
|
+
from mcp import CreateMessageResult
|
|
5
|
+
from mcp.server.session import ServerSession
|
|
6
|
+
from mcp.shared.context import LifespanContextT, RequestContext
|
|
7
|
+
from mcp.types import CreateMessageRequestParams as SamplingParams
|
|
8
|
+
from mcp.types import (
|
|
9
|
+
SamplingMessage,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
ServerSamplingHandler: TypeAlias = Callable[
|
|
13
|
+
[
|
|
14
|
+
list[SamplingMessage],
|
|
15
|
+
SamplingParams,
|
|
16
|
+
RequestContext[ServerSession, LifespanContextT],
|
|
17
|
+
],
|
|
18
|
+
str | CreateMessageResult | Awaitable[str | CreateMessageResult],
|
|
19
|
+
]
|
fastmcp/server/server.py
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
+
import json
|
|
6
7
|
import re
|
|
8
|
+
import secrets
|
|
7
9
|
import warnings
|
|
8
10
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
9
11
|
from contextlib import (
|
|
@@ -49,7 +51,7 @@ from fastmcp.prompts import Prompt, PromptManager
|
|
|
49
51
|
from fastmcp.prompts.prompt import FunctionPrompt
|
|
50
52
|
from fastmcp.resources import Resource, ResourceManager
|
|
51
53
|
from fastmcp.resources.template import ResourceTemplate
|
|
52
|
-
from fastmcp.server.auth
|
|
54
|
+
from fastmcp.server.auth import AuthProvider
|
|
53
55
|
from fastmcp.server.auth.registry import get_registered_provider
|
|
54
56
|
from fastmcp.server.http import (
|
|
55
57
|
StarletteWithLifespan,
|
|
@@ -69,6 +71,7 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|
|
69
71
|
|
|
70
72
|
if TYPE_CHECKING:
|
|
71
73
|
from fastmcp.client import Client
|
|
74
|
+
from fastmcp.client.sampling import ServerSamplingHandler
|
|
72
75
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
73
76
|
from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
|
|
74
77
|
from fastmcp.experimental.server.openapi.routing import (
|
|
@@ -166,12 +169,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
166
169
|
streamable_http_path: str | None = None,
|
|
167
170
|
json_response: bool | None = None,
|
|
168
171
|
stateless_http: bool | None = None,
|
|
172
|
+
sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
|
|
173
|
+
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
|
|
169
174
|
):
|
|
170
175
|
self.resource_prefix_format: Literal["protocol", "path"] = (
|
|
171
176
|
resource_prefix_format or fastmcp.settings.resource_prefix_format
|
|
172
177
|
)
|
|
173
178
|
|
|
174
179
|
self._additional_http_routes: list[BaseRoute] = []
|
|
180
|
+
self._mounted_servers: list[MountedServer] = []
|
|
175
181
|
self._tool_manager = ToolManager(
|
|
176
182
|
duplicate_behavior=on_duplicate_tools,
|
|
177
183
|
mask_error_details=mask_error_details,
|
|
@@ -192,8 +198,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
192
198
|
lifespan = default_lifespan
|
|
193
199
|
else:
|
|
194
200
|
self._has_lifespan = True
|
|
201
|
+
# Generate random ID if no name provided
|
|
195
202
|
self._mcp_server = LowLevelServer[LifespanResultT](
|
|
196
|
-
name=name or
|
|
203
|
+
name=name or self.generate_name(),
|
|
197
204
|
version=version,
|
|
198
205
|
instructions=instructions,
|
|
199
206
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
@@ -221,7 +228,27 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
221
228
|
|
|
222
229
|
# Set up MCP protocol handlers
|
|
223
230
|
self._setup_handlers()
|
|
224
|
-
|
|
231
|
+
|
|
232
|
+
# Handle dependencies with deprecation warning
|
|
233
|
+
# TODO: Remove dependencies parameter (deprecated in v2.11.4)
|
|
234
|
+
if dependencies is not None:
|
|
235
|
+
import warnings
|
|
236
|
+
|
|
237
|
+
warnings.warn(
|
|
238
|
+
"The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
|
|
239
|
+
"Please specify dependencies in a fastmcp.json configuration file instead:\n"
|
|
240
|
+
'{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
|
|
241
|
+
f"{json.dumps(dependencies)}\n }}\n}}\n"
|
|
242
|
+
"See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
|
|
243
|
+
DeprecationWarning,
|
|
244
|
+
stacklevel=2,
|
|
245
|
+
)
|
|
246
|
+
self.dependencies = (
|
|
247
|
+
dependencies or fastmcp.settings.server_dependencies
|
|
248
|
+
) # TODO: Remove (deprecated in v2.11.4)
|
|
249
|
+
|
|
250
|
+
self.sampling_handler = sampling_handler
|
|
251
|
+
self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
|
|
225
252
|
|
|
226
253
|
self.include_fastmcp_meta = (
|
|
227
254
|
include_fastmcp_meta
|
|
@@ -444,7 +471,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
444
471
|
Request and returns a Response.
|
|
445
472
|
|
|
446
473
|
Args:
|
|
447
|
-
path: URL path for the route (e.g., "/
|
|
474
|
+
path: URL path for the route (e.g., "/auth/callback")
|
|
448
475
|
methods: List of HTTP methods to support (e.g., ["GET", "POST"])
|
|
449
476
|
name: Optional name for the route (to reference this route with
|
|
450
477
|
Starlette's reverse URL lookup feature)
|
|
@@ -475,8 +502,26 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
475
502
|
|
|
476
503
|
return decorator
|
|
477
504
|
|
|
505
|
+
def _get_additional_http_routes(self) -> list[BaseRoute]:
|
|
506
|
+
"""Get all additional HTTP routes including from mounted servers.
|
|
507
|
+
|
|
508
|
+
Returns a list of all custom HTTP routes from this server and
|
|
509
|
+
recursively from all mounted servers.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
List of Starlette BaseRoute objects
|
|
513
|
+
"""
|
|
514
|
+
routes = list(self._additional_http_routes)
|
|
515
|
+
|
|
516
|
+
# Recursively get routes from mounted servers
|
|
517
|
+
for mounted_server in self._mounted_servers:
|
|
518
|
+
mounted_routes = mounted_server.server._get_additional_http_routes()
|
|
519
|
+
routes.extend(mounted_routes)
|
|
520
|
+
|
|
521
|
+
return routes
|
|
522
|
+
|
|
478
523
|
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
479
|
-
logger.debug("Handler called: list_tools")
|
|
524
|
+
logger.debug(f"[{self.name}] Handler called: list_tools")
|
|
480
525
|
|
|
481
526
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
482
527
|
tools = await self._list_tools()
|
|
@@ -520,7 +565,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
520
565
|
return await self._apply_middleware(mw_context, _handler)
|
|
521
566
|
|
|
522
567
|
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
523
|
-
logger.debug("Handler called: list_resources")
|
|
568
|
+
logger.debug(f"[{self.name}] Handler called: list_resources")
|
|
524
569
|
|
|
525
570
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
526
571
|
resources = await self._list_resources()
|
|
@@ -565,7 +610,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
565
610
|
return await self._apply_middleware(mw_context, _handler)
|
|
566
611
|
|
|
567
612
|
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
568
|
-
logger.debug("Handler called: list_resource_templates")
|
|
613
|
+
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
|
|
569
614
|
|
|
570
615
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
571
616
|
templates = await self._list_resource_templates()
|
|
@@ -610,7 +655,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
610
655
|
return await self._apply_middleware(mw_context, _handler)
|
|
611
656
|
|
|
612
657
|
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
613
|
-
logger.debug("Handler called: list_prompts")
|
|
658
|
+
logger.debug(f"[{self.name}] Handler called: list_prompts")
|
|
614
659
|
|
|
615
660
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
616
661
|
prompts = await self._list_prompts()
|
|
@@ -669,7 +714,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
669
714
|
Returns:
|
|
670
715
|
List of MCP Content objects containing the tool results
|
|
671
716
|
"""
|
|
672
|
-
logger.debug(
|
|
717
|
+
logger.debug(
|
|
718
|
+
f"[{self.name}] Handler called: call_tool %s with %s", key, arguments
|
|
719
|
+
)
|
|
673
720
|
|
|
674
721
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
675
722
|
try:
|
|
@@ -711,7 +758,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
711
758
|
|
|
712
759
|
Delegates to _read_resource, which should be overridden by FastMCP subclasses.
|
|
713
760
|
"""
|
|
714
|
-
logger.debug("Handler called: read_resource %s", uri)
|
|
761
|
+
logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
|
|
715
762
|
|
|
716
763
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
717
764
|
try:
|
|
@@ -766,7 +813,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
766
813
|
|
|
767
814
|
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
|
|
768
815
|
"""
|
|
769
|
-
logger.debug(
|
|
816
|
+
logger.debug(
|
|
817
|
+
f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
|
|
818
|
+
)
|
|
770
819
|
|
|
771
820
|
async with fastmcp.server.context.Context(fastmcp=self):
|
|
772
821
|
try:
|
|
@@ -984,7 +1033,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
984
1033
|
description=description,
|
|
985
1034
|
tags=tags,
|
|
986
1035
|
output_schema=output_schema,
|
|
987
|
-
annotations=annotations,
|
|
1036
|
+
annotations=cast(ToolAnnotations | None, annotations),
|
|
988
1037
|
exclude_args=exclude_args,
|
|
989
1038
|
meta=meta,
|
|
990
1039
|
serializer=self._tool_serializer,
|
|
@@ -1214,7 +1263,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1214
1263
|
mime_type=mime_type,
|
|
1215
1264
|
tags=tags,
|
|
1216
1265
|
enabled=enabled,
|
|
1217
|
-
annotations=annotations,
|
|
1266
|
+
annotations=cast(Annotations | None, annotations),
|
|
1218
1267
|
meta=meta,
|
|
1219
1268
|
)
|
|
1220
1269
|
self.add_template(template)
|
|
@@ -1229,7 +1278,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1229
1278
|
mime_type=mime_type,
|
|
1230
1279
|
tags=tags,
|
|
1231
1280
|
enabled=enabled,
|
|
1232
|
-
annotations=annotations,
|
|
1281
|
+
annotations=cast(Annotations | None, annotations),
|
|
1233
1282
|
meta=meta,
|
|
1234
1283
|
)
|
|
1235
1284
|
self.add_resource(resource)
|
|
@@ -1796,6 +1845,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1796
1845
|
server=server,
|
|
1797
1846
|
resource_prefix_format=self.resource_prefix_format,
|
|
1798
1847
|
)
|
|
1848
|
+
self._mounted_servers.append(mounted_server)
|
|
1799
1849
|
self._tool_manager.mount(mounted_server)
|
|
1800
1850
|
self._resource_manager.mount(mounted_server)
|
|
1801
1851
|
self._prompt_manager.mount(mounted_server)
|
|
@@ -1891,7 +1941,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1891
1941
|
# Import tools from the server
|
|
1892
1942
|
for key, tool in (await server.get_tools()).items():
|
|
1893
1943
|
if prefix:
|
|
1894
|
-
tool = tool.
|
|
1944
|
+
tool = tool.model_copy(key=f"{prefix}_{key}")
|
|
1895
1945
|
self._tool_manager.add_tool(tool)
|
|
1896
1946
|
|
|
1897
1947
|
# Import resources and templates from the server
|
|
@@ -1900,7 +1950,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1900
1950
|
resource_key = add_resource_prefix(
|
|
1901
1951
|
key, prefix, self.resource_prefix_format
|
|
1902
1952
|
)
|
|
1903
|
-
resource = resource.
|
|
1953
|
+
resource = resource.model_copy(
|
|
1954
|
+
update={"name": f"{prefix}_{resource.name}"}, key=resource_key
|
|
1955
|
+
)
|
|
1904
1956
|
self._resource_manager.add_resource(resource)
|
|
1905
1957
|
|
|
1906
1958
|
for key, template in (await server.get_resource_templates()).items():
|
|
@@ -1908,19 +1960,23 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1908
1960
|
template_key = add_resource_prefix(
|
|
1909
1961
|
key, prefix, self.resource_prefix_format
|
|
1910
1962
|
)
|
|
1911
|
-
template = template.
|
|
1963
|
+
template = template.model_copy(
|
|
1964
|
+
update={"name": f"{prefix}_{template.name}"}, key=template_key
|
|
1965
|
+
)
|
|
1912
1966
|
self._resource_manager.add_template(template)
|
|
1913
1967
|
|
|
1914
1968
|
# Import prompts from the server
|
|
1915
1969
|
for key, prompt in (await server.get_prompts()).items():
|
|
1916
1970
|
if prefix:
|
|
1917
|
-
prompt = prompt.
|
|
1971
|
+
prompt = prompt.model_copy(key=f"{prefix}_{key}")
|
|
1918
1972
|
self._prompt_manager.add_prompt(prompt)
|
|
1919
1973
|
|
|
1920
1974
|
if prefix:
|
|
1921
|
-
logger.debug(
|
|
1975
|
+
logger.debug(
|
|
1976
|
+
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
|
|
1977
|
+
)
|
|
1922
1978
|
else:
|
|
1923
|
-
logger.debug(f"Imported server {server.name}")
|
|
1979
|
+
logger.debug(f"[{self.name}] Imported server {server.name}")
|
|
1924
1980
|
|
|
1925
1981
|
@classmethod
|
|
1926
1982
|
def from_openapi(
|
|
@@ -2147,6 +2203,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2147
2203
|
|
|
2148
2204
|
return True
|
|
2149
2205
|
|
|
2206
|
+
@classmethod
|
|
2207
|
+
def generate_name(cls, name: str | None = None) -> str:
|
|
2208
|
+
class_name = cls.__name__
|
|
2209
|
+
|
|
2210
|
+
if name is None:
|
|
2211
|
+
return f"{class_name}-{secrets.token_hex(2)}"
|
|
2212
|
+
else:
|
|
2213
|
+
return f"{class_name}-{name}-{secrets.token_hex(2)}"
|
|
2214
|
+
|
|
2150
2215
|
|
|
2151
2216
|
@dataclass
|
|
2152
2217
|
class MountedServer:
|
fastmcp/settings.py
CHANGED
|
@@ -146,6 +146,7 @@ class Settings(BaseSettings):
|
|
|
146
146
|
|
|
147
147
|
test_mode: bool = False
|
|
148
148
|
|
|
149
|
+
log_enabled: bool = True
|
|
149
150
|
log_level: LOG_LEVEL = "INFO"
|
|
150
151
|
|
|
151
152
|
@field_validator("log_level", mode="before")
|
|
@@ -222,9 +223,9 @@ class Settings(BaseSettings):
|
|
|
222
223
|
# HTTP settings
|
|
223
224
|
host: str = "127.0.0.1"
|
|
224
225
|
port: int = 8000
|
|
225
|
-
sse_path: str = "/sse
|
|
226
|
+
sse_path: str = "/sse"
|
|
226
227
|
message_path: str = "/messages/"
|
|
227
|
-
streamable_http_path: str = "/mcp
|
|
228
|
+
streamable_http_path: str = "/mcp"
|
|
228
229
|
debug: bool = False
|
|
229
230
|
|
|
230
231
|
# error handling
|
|
@@ -314,12 +315,26 @@ class Settings(BaseSettings):
|
|
|
314
315
|
Whether to include FastMCP meta in the server's MCP responses.
|
|
315
316
|
If True, a `_fastmcp` key will be added to the `meta` field of
|
|
316
317
|
all MCP component responses. This key will contain a dict of
|
|
317
|
-
various FastMCP-specific metadata, such as tags.
|
|
318
|
+
various FastMCP-specific metadata, such as tags.
|
|
318
319
|
"""
|
|
319
320
|
),
|
|
320
321
|
),
|
|
321
322
|
] = True
|
|
322
323
|
|
|
324
|
+
mounted_components_raise_on_load_error: Annotated[
|
|
325
|
+
bool,
|
|
326
|
+
Field(
|
|
327
|
+
default=False,
|
|
328
|
+
description=inspect.cleandoc(
|
|
329
|
+
"""
|
|
330
|
+
If True, errors encountered when loading mounted components (tools, resources, prompts)
|
|
331
|
+
will be raised instead of logged as warnings. This is useful for debugging
|
|
332
|
+
but will interrupt normal operation.
|
|
333
|
+
"""
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
] = False
|
|
337
|
+
|
|
323
338
|
|
|
324
339
|
def __getattr__(name: str):
|
|
325
340
|
"""
|
fastmcp/tools/tool.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import warnings
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from typing import (
|
|
@@ -19,6 +20,7 @@ from mcp.types import ContentBlock, TextContent, ToolAnnotations
|
|
|
19
20
|
from mcp.types import Tool as MCPTool
|
|
20
21
|
from pydantic import Field, PydanticSchemaGenerationError
|
|
21
22
|
|
|
23
|
+
import fastmcp
|
|
22
24
|
from fastmcp.server.dependencies import get_context
|
|
23
25
|
from fastmcp.utilities.components import FastMCPComponent
|
|
24
26
|
from fastmcp.utilities.json_schema import compress_schema
|
|
@@ -199,17 +201,18 @@ class Tool(FastMCPComponent):
|
|
|
199
201
|
def from_tool(
|
|
200
202
|
cls,
|
|
201
203
|
tool: Tool,
|
|
202
|
-
|
|
204
|
+
*,
|
|
203
205
|
name: str | None = None,
|
|
204
206
|
title: str | None | NotSetT = NotSet,
|
|
205
|
-
transform_args: dict[str, ArgTransform] | None = None,
|
|
206
207
|
description: str | None | NotSetT = NotSet,
|
|
207
208
|
tags: set[str] | None = None,
|
|
208
|
-
annotations: ToolAnnotations | None =
|
|
209
|
-
output_schema: dict[str, Any] | None | Literal[False] =
|
|
209
|
+
annotations: ToolAnnotations | None | NotSetT = NotSet,
|
|
210
|
+
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
210
211
|
serializer: Callable[[Any], str] | None = None,
|
|
211
212
|
meta: dict[str, Any] | None | NotSetT = NotSet,
|
|
213
|
+
transform_args: dict[str, ArgTransform] | None = None,
|
|
212
214
|
enabled: bool | None = None,
|
|
215
|
+
transform_fn: Callable[..., Any] | None = None,
|
|
213
216
|
) -> TransformedTool:
|
|
214
217
|
from fastmcp.tools.tool_transform import TransformedTool
|
|
215
218
|
|
|
@@ -255,16 +258,26 @@ class FunctionTool(Tool):
|
|
|
255
258
|
raise ValueError("You must provide a name for lambda functions")
|
|
256
259
|
|
|
257
260
|
if isinstance(output_schema, NotSetT):
|
|
258
|
-
|
|
261
|
+
final_output_schema = parsed_fn.output_schema
|
|
259
262
|
elif output_schema is False:
|
|
260
|
-
|
|
263
|
+
# Handle False as deprecated synonym for None (deprecated in 2.11.4)
|
|
264
|
+
if fastmcp.settings.deprecation_warnings:
|
|
265
|
+
warnings.warn(
|
|
266
|
+
"Passing output_schema=False is deprecated. Use output_schema=None instead.",
|
|
267
|
+
DeprecationWarning,
|
|
268
|
+
stacklevel=2,
|
|
269
|
+
)
|
|
270
|
+
final_output_schema = None
|
|
271
|
+
else:
|
|
272
|
+
# At this point output_schema is not NotSetT and not False, so it must be dict | None
|
|
273
|
+
final_output_schema = output_schema
|
|
261
274
|
# Note: explicit schemas (dict) are used as-is without auto-wrapping
|
|
262
275
|
|
|
263
276
|
# Validate that explicit schemas are object type for structured content
|
|
264
|
-
if
|
|
265
|
-
if
|
|
277
|
+
if final_output_schema is not None and isinstance(final_output_schema, dict):
|
|
278
|
+
if final_output_schema.get("type") != "object":
|
|
266
279
|
raise ValueError(
|
|
267
|
-
f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {
|
|
280
|
+
f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {final_output_schema!r}'
|
|
268
281
|
)
|
|
269
282
|
|
|
270
283
|
return cls(
|
|
@@ -273,7 +286,7 @@ class FunctionTool(Tool):
|
|
|
273
286
|
title=title,
|
|
274
287
|
description=description or parsed_fn.description,
|
|
275
288
|
parameters=parsed_fn.input_schema,
|
|
276
|
-
output_schema=
|
|
289
|
+
output_schema=final_output_schema,
|
|
277
290
|
annotations=annotations,
|
|
278
291
|
tags=tags or set(),
|
|
279
292
|
serializer=serializer,
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -75,7 +75,9 @@ class ToolManager:
|
|
|
75
75
|
child_dict = {t.key: t for t in child_results}
|
|
76
76
|
if mounted.prefix:
|
|
77
77
|
for tool in child_dict.values():
|
|
78
|
-
prefixed_tool = tool.
|
|
78
|
+
prefixed_tool = tool.model_copy(
|
|
79
|
+
key=f"{mounted.prefix}_{tool.key}"
|
|
80
|
+
)
|
|
79
81
|
all_tools[prefixed_tool.key] = prefixed_tool
|
|
80
82
|
else:
|
|
81
83
|
all_tools.update(child_dict)
|
|
@@ -84,6 +86,8 @@ class ToolManager:
|
|
|
84
86
|
logger.warning(
|
|
85
87
|
f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
86
88
|
)
|
|
89
|
+
if settings.mounted_components_raise_on_load_error:
|
|
90
|
+
raise
|
|
87
91
|
continue
|
|
88
92
|
|
|
89
93
|
# Finally, add local tools, which always take precedence
|