fastmcp 2.14.0__py3-none-any.whl → 2.14.2__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/client/client.py +79 -12
- fastmcp/client/sampling/__init__.py +69 -0
- fastmcp/client/sampling/handlers/__init__.py +0 -0
- fastmcp/client/sampling/handlers/anthropic.py +387 -0
- fastmcp/client/sampling/handlers/openai.py +399 -0
- fastmcp/client/tasks.py +0 -63
- fastmcp/client/transports.py +35 -16
- fastmcp/experimental/sampling/handlers/__init__.py +5 -0
- fastmcp/experimental/sampling/handlers/openai.py +4 -169
- fastmcp/prompts/prompt.py +5 -5
- fastmcp/prompts/prompt_manager.py +3 -4
- fastmcp/resources/resource.py +4 -4
- fastmcp/resources/resource_manager.py +9 -14
- fastmcp/resources/template.py +5 -5
- fastmcp/server/auth/auth.py +20 -5
- fastmcp/server/auth/oauth_proxy.py +73 -15
- fastmcp/server/auth/providers/supabase.py +11 -6
- fastmcp/server/context.py +448 -113
- fastmcp/server/dependencies.py +5 -0
- fastmcp/server/elicitation.py +7 -3
- fastmcp/server/middleware/error_handling.py +1 -1
- fastmcp/server/openapi/components.py +2 -4
- fastmcp/server/proxy.py +3 -3
- fastmcp/server/sampling/__init__.py +10 -0
- fastmcp/server/sampling/run.py +301 -0
- fastmcp/server/sampling/sampling_tool.py +108 -0
- fastmcp/server/server.py +84 -78
- fastmcp/server/tasks/converters.py +2 -1
- fastmcp/tools/tool.py +8 -6
- fastmcp/tools/tool_manager.py +5 -7
- fastmcp/utilities/cli.py +23 -43
- fastmcp/utilities/json_schema.py +40 -0
- fastmcp/utilities/openapi/schemas.py +4 -4
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/METADATA +8 -3
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/RECORD +38 -34
- fastmcp/client/sampling.py +0 -56
- fastmcp/experimental/sampling/handlers/base.py +0 -21
- fastmcp/server/sampling/handler.py +0 -19
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -46,10 +46,10 @@ from mcp.types import (
|
|
|
46
46
|
GetPromptResult,
|
|
47
47
|
ToolAnnotations,
|
|
48
48
|
)
|
|
49
|
-
from mcp.types import Prompt as
|
|
50
|
-
from mcp.types import Resource as
|
|
51
|
-
from mcp.types import ResourceTemplate as
|
|
52
|
-
from mcp.types import Tool as
|
|
49
|
+
from mcp.types import Prompt as SDKPrompt
|
|
50
|
+
from mcp.types import Resource as SDKResource
|
|
51
|
+
from mcp.types import ResourceTemplate as SDKResourceTemplate
|
|
52
|
+
from mcp.types import Tool as SDKTool
|
|
53
53
|
from pydantic import AnyUrl
|
|
54
54
|
from starlette.middleware import Middleware as ASGIMiddleware
|
|
55
55
|
from starlette.requests import Request
|
|
@@ -94,34 +94,17 @@ from fastmcp.utilities.types import NotSet, NotSetT
|
|
|
94
94
|
if TYPE_CHECKING:
|
|
95
95
|
from fastmcp.client import Client
|
|
96
96
|
from fastmcp.client.client import FastMCP1Server
|
|
97
|
+
from fastmcp.client.sampling import SamplingHandler
|
|
97
98
|
from fastmcp.client.transports import ClientTransport, ClientTransportT
|
|
98
99
|
from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
|
|
99
100
|
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
|
|
100
101
|
from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
|
|
101
102
|
from fastmcp.server.proxy import FastMCPProxy
|
|
102
|
-
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
|
103
103
|
from fastmcp.tools.tool import ToolResultSerializerType
|
|
104
104
|
|
|
105
105
|
logger = get_logger(__name__)
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
|
|
109
|
-
"""Create a wrapper function with a custom __name__ for Docket registration.
|
|
110
|
-
|
|
111
|
-
Docket uses fn.__name__ as the key for function registration and lookup.
|
|
112
|
-
When mounting servers, we need unique names to avoid collisions between
|
|
113
|
-
mounted servers that have identically-named functions.
|
|
114
|
-
"""
|
|
115
|
-
import functools
|
|
116
|
-
|
|
117
|
-
@functools.wraps(fn)
|
|
118
|
-
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
119
|
-
return await fn(*args, **kwargs)
|
|
120
|
-
|
|
121
|
-
wrapper.__name__ = name
|
|
122
|
-
return wrapper
|
|
123
|
-
|
|
124
|
-
|
|
125
108
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
126
109
|
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
|
127
110
|
|
|
@@ -208,7 +191,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
208
191
|
streamable_http_path: str | None = None,
|
|
209
192
|
json_response: bool | None = None,
|
|
210
193
|
stateless_http: bool | None = None,
|
|
211
|
-
sampling_handler:
|
|
194
|
+
sampling_handler: SamplingHandler | None = None,
|
|
212
195
|
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
|
|
213
196
|
):
|
|
214
197
|
# Resolve server default for background task support
|
|
@@ -288,9 +271,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
288
271
|
# Set up MCP protocol handlers
|
|
289
272
|
self._setup_handlers()
|
|
290
273
|
|
|
291
|
-
self.sampling_handler:
|
|
292
|
-
sampling_handler
|
|
293
|
-
)
|
|
274
|
+
self.sampling_handler: SamplingHandler | None = sampling_handler
|
|
294
275
|
self.sampling_handler_behavior: Literal["always", "fallback"] = (
|
|
295
276
|
sampling_handler_behavior or "fallback"
|
|
296
277
|
)
|
|
@@ -439,7 +420,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
439
420
|
isinstance(tool, FunctionTool)
|
|
440
421
|
and tool.task_config.mode != "forbidden"
|
|
441
422
|
):
|
|
442
|
-
docket.register(tool.fn)
|
|
423
|
+
docket.register(tool.fn, names=[tool.key])
|
|
443
424
|
|
|
444
425
|
for prompt in self._prompt_manager._prompts.values():
|
|
445
426
|
if (
|
|
@@ -447,27 +428,30 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
447
428
|
and prompt.task_config.mode != "forbidden"
|
|
448
429
|
):
|
|
449
430
|
# task execution requires async fn (validated at creation time)
|
|
450
|
-
docket.register(
|
|
431
|
+
docket.register(
|
|
432
|
+
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
|
433
|
+
names=[prompt.key],
|
|
434
|
+
)
|
|
451
435
|
|
|
452
436
|
for resource in self._resource_manager._resources.values():
|
|
453
437
|
if (
|
|
454
438
|
isinstance(resource, FunctionResource)
|
|
455
439
|
and resource.task_config.mode != "forbidden"
|
|
456
440
|
):
|
|
457
|
-
docket.register(resource.fn)
|
|
441
|
+
docket.register(resource.fn, names=[resource.name])
|
|
458
442
|
|
|
459
443
|
for template in self._resource_manager._templates.values():
|
|
460
444
|
if (
|
|
461
445
|
isinstance(template, FunctionResourceTemplate)
|
|
462
446
|
and template.task_config.mode != "forbidden"
|
|
463
447
|
):
|
|
464
|
-
docket.register(template.fn)
|
|
448
|
+
docket.register(template.fn, names=[template.name])
|
|
465
449
|
|
|
466
450
|
# Also register functions from mounted servers so tasks can
|
|
467
451
|
# execute in the parent's Docket context
|
|
468
452
|
for mounted in self._mounted_servers:
|
|
469
453
|
await self._register_mounted_server_functions(
|
|
470
|
-
mounted.server, docket, mounted.prefix
|
|
454
|
+
mounted.server, docket, mounted.prefix, mounted.tool_names
|
|
471
455
|
)
|
|
472
456
|
|
|
473
457
|
# Set Docket in ContextVar so CurrentDocket can access it
|
|
@@ -491,12 +475,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
491
475
|
try:
|
|
492
476
|
yield
|
|
493
477
|
finally:
|
|
494
|
-
# Cancel worker task on exit with timeout to prevent hanging
|
|
495
478
|
worker_task.cancel()
|
|
496
|
-
with suppress(
|
|
497
|
-
|
|
498
|
-
):
|
|
499
|
-
await asyncio.wait_for(worker_task, timeout=2.0)
|
|
479
|
+
with suppress(asyncio.CancelledError):
|
|
480
|
+
await worker_task
|
|
500
481
|
finally:
|
|
501
482
|
_current_worker.reset(worker_token)
|
|
502
483
|
finally:
|
|
@@ -509,7 +490,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
509
490
|
_current_server.reset(server_token)
|
|
510
491
|
|
|
511
492
|
async def _register_mounted_server_functions(
|
|
512
|
-
self,
|
|
493
|
+
self,
|
|
494
|
+
server: FastMCP,
|
|
495
|
+
docket: Docket,
|
|
496
|
+
prefix: str | None,
|
|
497
|
+
tool_names: dict[str, str] | None = None,
|
|
513
498
|
) -> None:
|
|
514
499
|
"""Register task-enabled functions from a mounted server with Docket.
|
|
515
500
|
|
|
@@ -521,14 +506,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
521
506
|
docket: The Docket instance to register with
|
|
522
507
|
prefix: The mount prefix to prepend to function names (matches
|
|
523
508
|
client-facing tool/prompt names)
|
|
509
|
+
tool_names: Optional mapping of original tool names to custom names
|
|
524
510
|
"""
|
|
525
511
|
# Register tools with prefixed names to avoid collisions
|
|
526
512
|
for tool in server._tool_manager._tools.values():
|
|
527
513
|
if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
|
|
528
|
-
#
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
514
|
+
# Apply tool_names override first, then prefix (matches get_tools logic)
|
|
515
|
+
if tool_names and tool.key in tool_names:
|
|
516
|
+
fn_name = tool_names[tool.key]
|
|
517
|
+
elif prefix:
|
|
518
|
+
fn_name = f"{prefix}_{tool.key}"
|
|
519
|
+
else:
|
|
520
|
+
fn_name = tool.key
|
|
521
|
+
docket.register(tool.fn, names=[fn_name])
|
|
532
522
|
|
|
533
523
|
# Register prompts with prefixed names
|
|
534
524
|
for prompt in server._prompt_manager._prompts.values():
|
|
@@ -537,10 +527,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
537
527
|
and prompt.task_config.mode != "forbidden"
|
|
538
528
|
):
|
|
539
529
|
fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
|
|
540
|
-
|
|
541
|
-
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
|
530
|
+
docket.register(
|
|
531
|
+
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
|
532
|
+
names=[fn_name],
|
|
542
533
|
)
|
|
543
|
-
docket.register(named_fn)
|
|
544
534
|
|
|
545
535
|
# Register resources with prefixed names (use name, not key/URI)
|
|
546
536
|
for resource in server._resource_manager._resources.values():
|
|
@@ -549,8 +539,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
549
539
|
and resource.task_config.mode != "forbidden"
|
|
550
540
|
):
|
|
551
541
|
fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
|
|
552
|
-
|
|
553
|
-
docket.register(named_fn)
|
|
542
|
+
docket.register(resource.fn, names=[fn_name])
|
|
554
543
|
|
|
555
544
|
# Register resource templates with prefixed names (use name, not key/URI)
|
|
556
545
|
for template in server._resource_manager._templates.values():
|
|
@@ -559,8 +548,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
559
548
|
and template.task_config.mode != "forbidden"
|
|
560
549
|
):
|
|
561
550
|
fn_name = f"{prefix}_{template.name}" if prefix else template.name
|
|
562
|
-
|
|
563
|
-
docket.register(named_fn)
|
|
551
|
+
docket.register(template.fn, names=[fn_name])
|
|
564
552
|
|
|
565
553
|
# Recursively register from nested mounted servers with accumulated prefix
|
|
566
554
|
for nested in server._mounted_servers:
|
|
@@ -570,7 +558,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
570
558
|
else (prefix or nested.prefix)
|
|
571
559
|
)
|
|
572
560
|
await self._register_mounted_server_functions(
|
|
573
|
-
nested.server, docket, nested_prefix
|
|
561
|
+
nested.server, docket, nested_prefix, nested.tool_names
|
|
574
562
|
)
|
|
575
563
|
|
|
576
564
|
@asynccontextmanager
|
|
@@ -938,7 +926,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
938
926
|
try:
|
|
939
927
|
child_tools = await mounted.server.get_tools()
|
|
940
928
|
for key, tool in child_tools.items():
|
|
941
|
-
|
|
929
|
+
# Check for manual override first, then apply prefix
|
|
930
|
+
if mounted.tool_names and key in mounted.tool_names:
|
|
931
|
+
new_key = mounted.tool_names[key]
|
|
932
|
+
elif mounted.prefix:
|
|
933
|
+
new_key = f"{mounted.prefix}_{key}"
|
|
934
|
+
else:
|
|
935
|
+
new_key = key
|
|
942
936
|
all_tools[new_key] = tool.model_copy(key=new_key)
|
|
943
937
|
except Exception as e:
|
|
944
938
|
logger.warning(
|
|
@@ -1153,7 +1147,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1153
1147
|
|
|
1154
1148
|
return routes
|
|
1155
1149
|
|
|
1156
|
-
async def _list_tools_mcp(self) -> list[
|
|
1150
|
+
async def _list_tools_mcp(self) -> list[SDKTool]:
|
|
1157
1151
|
"""
|
|
1158
1152
|
List all available tools, in the format expected by the low-level MCP
|
|
1159
1153
|
server.
|
|
@@ -1218,9 +1212,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1218
1212
|
if not self._should_enable_component(tool):
|
|
1219
1213
|
continue
|
|
1220
1214
|
|
|
1221
|
-
|
|
1222
|
-
if mounted.
|
|
1215
|
+
# Check for manual override first, then apply prefix
|
|
1216
|
+
if mounted.tool_names and tool.key in mounted.tool_names:
|
|
1217
|
+
key = mounted.tool_names[tool.key]
|
|
1218
|
+
elif mounted.prefix:
|
|
1223
1219
|
key = f"{mounted.prefix}_{tool.key}"
|
|
1220
|
+
else:
|
|
1221
|
+
key = tool.key
|
|
1222
|
+
|
|
1223
|
+
if key != tool.key:
|
|
1224
1224
|
tool = tool.model_copy(key=key)
|
|
1225
1225
|
# Later mounted servers override earlier ones
|
|
1226
1226
|
all_tools[key] = tool
|
|
@@ -1237,7 +1237,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1237
1237
|
|
|
1238
1238
|
return list(all_tools.values())
|
|
1239
1239
|
|
|
1240
|
-
async def _list_resources_mcp(self) -> list[
|
|
1240
|
+
async def _list_resources_mcp(self) -> list[SDKResource]:
|
|
1241
1241
|
"""
|
|
1242
1242
|
List all available resources, in the format expected by the low-level MCP
|
|
1243
1243
|
server.
|
|
@@ -1326,7 +1326,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1326
1326
|
|
|
1327
1327
|
return list(all_resources.values())
|
|
1328
1328
|
|
|
1329
|
-
async def _list_resource_templates_mcp(self) -> list[
|
|
1329
|
+
async def _list_resource_templates_mcp(self) -> list[SDKResourceTemplate]:
|
|
1330
1330
|
"""
|
|
1331
1331
|
List all available resource templates, in the format expected by the low-level MCP
|
|
1332
1332
|
server.
|
|
@@ -1420,7 +1420,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1420
1420
|
|
|
1421
1421
|
return list(all_templates.values())
|
|
1422
1422
|
|
|
1423
|
-
async def _list_prompts_mcp(self) -> list[
|
|
1423
|
+
async def _list_prompts_mcp(self) -> list[SDKPrompt]:
|
|
1424
1424
|
"""
|
|
1425
1425
|
List all available prompts, in the format expected by the low-level MCP
|
|
1426
1426
|
server.
|
|
@@ -1490,9 +1490,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1490
1490
|
if not self._should_enable_component(prompt):
|
|
1491
1491
|
continue
|
|
1492
1492
|
|
|
1493
|
-
|
|
1493
|
+
# Apply prefix to prompt key
|
|
1494
1494
|
if mounted.prefix:
|
|
1495
1495
|
key = f"{mounted.prefix}_{prompt.key}"
|
|
1496
|
+
else:
|
|
1497
|
+
key = prompt.key
|
|
1498
|
+
|
|
1499
|
+
if key != prompt.key:
|
|
1496
1500
|
prompt = prompt.model_copy(key=key)
|
|
1497
1501
|
# Later mounted servers override earlier ones
|
|
1498
1502
|
all_prompts[key] = prompt
|
|
@@ -1632,7 +1636,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1632
1636
|
# Try mounted servers in reverse order (later wins)
|
|
1633
1637
|
for mounted in reversed(self._mounted_servers):
|
|
1634
1638
|
try_name = tool_name
|
|
1635
|
-
|
|
1639
|
+
|
|
1640
|
+
# First check if tool_name is an overridden name (reverse lookup)
|
|
1641
|
+
if mounted.tool_names:
|
|
1642
|
+
for orig_key, override_name in mounted.tool_names.items():
|
|
1643
|
+
if override_name == tool_name:
|
|
1644
|
+
try_name = orig_key
|
|
1645
|
+
break
|
|
1646
|
+
else:
|
|
1647
|
+
# Not an override, try standard prefix stripping
|
|
1648
|
+
if mounted.prefix:
|
|
1649
|
+
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1650
|
+
continue
|
|
1651
|
+
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
1652
|
+
elif mounted.prefix:
|
|
1636
1653
|
if not tool_name.startswith(f"{mounted.prefix}_"):
|
|
1637
1654
|
continue
|
|
1638
1655
|
try_name = tool_name[len(mounted.prefix) + 1 :]
|
|
@@ -1968,8 +1985,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1968
1985
|
output_schema: Optional JSON schema for the tool's output
|
|
1969
1986
|
annotations: Optional annotations about the tool's behavior
|
|
1970
1987
|
exclude_args: Optional list of argument names to exclude from the tool schema.
|
|
1971
|
-
|
|
1972
|
-
injection with `Depends()` for better lifecycle management.
|
|
1988
|
+
Deprecated: Use `Depends()` for dependency injection instead.
|
|
1973
1989
|
meta: Optional meta information about the tool
|
|
1974
1990
|
enabled: Optional boolean to enable or disable the tool
|
|
1975
1991
|
|
|
@@ -2480,10 +2496,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2480
2496
|
"""
|
|
2481
2497
|
# Display server banner
|
|
2482
2498
|
if show_banner:
|
|
2483
|
-
log_server_banner(
|
|
2484
|
-
server=self,
|
|
2485
|
-
transport="stdio",
|
|
2486
|
-
)
|
|
2499
|
+
log_server_banner(server=self)
|
|
2487
2500
|
|
|
2488
2501
|
with temporary_log_level(log_level):
|
|
2489
2502
|
async with self._lifespan_manager():
|
|
@@ -2546,22 +2559,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2546
2559
|
stateless_http=stateless_http,
|
|
2547
2560
|
)
|
|
2548
2561
|
|
|
2549
|
-
# Get the path for the server URL
|
|
2550
|
-
server_path = (
|
|
2551
|
-
app.state.path.lstrip("/")
|
|
2552
|
-
if hasattr(app, "state") and hasattr(app.state, "path")
|
|
2553
|
-
else path or ""
|
|
2554
|
-
)
|
|
2555
|
-
|
|
2556
2562
|
# Display server banner
|
|
2557
2563
|
if show_banner:
|
|
2558
|
-
log_server_banner(
|
|
2559
|
-
server=self,
|
|
2560
|
-
transport=transport,
|
|
2561
|
-
host=host,
|
|
2562
|
-
port=port,
|
|
2563
|
-
path=server_path,
|
|
2564
|
-
)
|
|
2564
|
+
log_server_banner(server=self)
|
|
2565
2565
|
uvicorn_config_from_user = uvicorn_config or {}
|
|
2566
2566
|
|
|
2567
2567
|
config_kwargs: dict[str, Any] = {
|
|
@@ -2651,6 +2651,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2651
2651
|
server: FastMCP[LifespanResultT],
|
|
2652
2652
|
prefix: str | None = None,
|
|
2653
2653
|
as_proxy: bool | None = None,
|
|
2654
|
+
tool_names: dict[str, str] | None = None,
|
|
2654
2655
|
) -> None:
|
|
2655
2656
|
"""Mount another FastMCP server on this server with an optional prefix.
|
|
2656
2657
|
|
|
@@ -2695,6 +2696,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2695
2696
|
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
|
2696
2697
|
automatically determined based on whether the server has a custom lifespan
|
|
2697
2698
|
(True if it has a custom lifespan, False otherwise).
|
|
2699
|
+
tool_names: Optional mapping of original tool names to custom names. Use this
|
|
2700
|
+
to override prefixed names. Keys are the original tool names from the
|
|
2701
|
+
mounted server.
|
|
2698
2702
|
"""
|
|
2699
2703
|
from fastmcp.server.proxy import FastMCPProxy
|
|
2700
2704
|
|
|
@@ -2715,6 +2719,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2715
2719
|
mounted_server = MountedServer(
|
|
2716
2720
|
prefix=prefix,
|
|
2717
2721
|
server=server,
|
|
2722
|
+
tool_names=tool_names,
|
|
2718
2723
|
)
|
|
2719
2724
|
self._mounted_servers.append(mounted_server)
|
|
2720
2725
|
|
|
@@ -2974,6 +2979,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
2974
2979
|
class MountedServer:
|
|
2975
2980
|
prefix: str | None
|
|
2976
2981
|
server: FastMCP[Any]
|
|
2982
|
+
tool_names: dict[str, str] | None = None
|
|
2977
2983
|
|
|
2978
2984
|
|
|
2979
2985
|
def add_resource_prefix(uri: str, prefix: str) -> str:
|
|
@@ -123,7 +123,8 @@ async def convert_prompt_result(
|
|
|
123
123
|
messages: list[mcp.types.PromptMessage] = []
|
|
124
124
|
for msg in raw_value:
|
|
125
125
|
if isinstance(msg, PromptMessage):
|
|
126
|
-
|
|
126
|
+
# PromptMessage is imported from mcp.types - use directly
|
|
127
|
+
messages.append(msg)
|
|
127
128
|
elif isinstance(msg, str):
|
|
128
129
|
messages.append(
|
|
129
130
|
mcp.types.PromptMessage(
|
fastmcp/tools/tool.py
CHANGED
|
@@ -32,7 +32,7 @@ import fastmcp
|
|
|
32
32
|
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
|
33
33
|
from fastmcp.server.tasks.config import TaskConfig
|
|
34
34
|
from fastmcp.utilities.components import FastMCPComponent
|
|
35
|
-
from fastmcp.utilities.json_schema import compress_schema
|
|
35
|
+
from fastmcp.utilities.json_schema import compress_schema, resolve_root_ref
|
|
36
36
|
from fastmcp.utilities.logging import get_logger
|
|
37
37
|
from fastmcp.utilities.types import (
|
|
38
38
|
Audio,
|
|
@@ -321,11 +321,9 @@ class FunctionTool(Tool):
|
|
|
321
321
|
"""Create a Tool from a function."""
|
|
322
322
|
if exclude_args and fastmcp.settings.deprecation_warnings:
|
|
323
323
|
warnings.warn(
|
|
324
|
-
"The `exclude_args` parameter
|
|
325
|
-
"
|
|
326
|
-
"
|
|
327
|
-
"`exclude_args` will continue to work until then. "
|
|
328
|
-
"See https://gofastmcp.com/docs/servers/tools for examples.",
|
|
324
|
+
"The `exclude_args` parameter is deprecated as of FastMCP 2.14. "
|
|
325
|
+
"Use dependency injection with `Depends()` instead for better lifecycle management. "
|
|
326
|
+
"See https://gofastmcp.com/servers/dependencies for examples.",
|
|
329
327
|
DeprecationWarning,
|
|
330
328
|
stacklevel=2,
|
|
331
329
|
)
|
|
@@ -561,6 +559,10 @@ class ParsedFunction:
|
|
|
561
559
|
|
|
562
560
|
output_schema = compress_schema(output_schema, prune_titles=True)
|
|
563
561
|
|
|
562
|
+
# Resolve root-level $ref to meet MCP spec requirement for type: object
|
|
563
|
+
# Self-referential Pydantic models generate schemas with $ref at root
|
|
564
|
+
output_schema = resolve_root_ref(output_schema)
|
|
565
|
+
|
|
564
566
|
except PydanticSchemaGenerationError as e:
|
|
565
567
|
if "_UnserializableType" not in str(e):
|
|
566
568
|
logger.debug(f"Unable to generate schema for type {output_type!r}")
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -8,7 +8,7 @@ from mcp.types import ToolAnnotations
|
|
|
8
8
|
from pydantic import ValidationError
|
|
9
9
|
|
|
10
10
|
from fastmcp import settings
|
|
11
|
-
from fastmcp.exceptions import NotFoundError, ToolError
|
|
11
|
+
from fastmcp.exceptions import FastMCPError, NotFoundError, ToolError
|
|
12
12
|
from fastmcp.settings import DuplicateBehavior
|
|
13
13
|
from fastmcp.tools.tool import Tool, ToolResult
|
|
14
14
|
from fastmcp.tools.tool_transform import (
|
|
@@ -158,12 +158,10 @@ class ToolManager:
|
|
|
158
158
|
tool = await self.get_tool(key)
|
|
159
159
|
try:
|
|
160
160
|
return await tool.run(arguments)
|
|
161
|
-
except
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
logger.exception(f"Error calling tool {key!r}")
|
|
166
|
-
raise e
|
|
161
|
+
except FastMCPError:
|
|
162
|
+
raise
|
|
163
|
+
except ValidationError:
|
|
164
|
+
raise
|
|
167
165
|
except Exception as e:
|
|
168
166
|
logger.exception(f"Error calling tool {key!r}")
|
|
169
167
|
if self.mask_error_details:
|
fastmcp/utilities/cli.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
from pydantic import ValidationError
|
|
9
9
|
from rich.align import Align
|
|
@@ -197,23 +197,8 @@ LOGO_ASCII_4 = (
|
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
|
|
200
|
-
def log_server_banner(
|
|
201
|
-
server
|
|
202
|
-
transport: Literal["stdio", "http", "sse", "streamable-http"],
|
|
203
|
-
*,
|
|
204
|
-
host: str | None = None,
|
|
205
|
-
port: int | None = None,
|
|
206
|
-
path: str | None = None,
|
|
207
|
-
) -> None:
|
|
208
|
-
"""Creates and logs a formatted banner with server information and logo.
|
|
209
|
-
|
|
210
|
-
Args:
|
|
211
|
-
transport: The transport protocol being used
|
|
212
|
-
server_name: Optional server name to display
|
|
213
|
-
host: Host address (for HTTP transports)
|
|
214
|
-
port: Port number (for HTTP transports)
|
|
215
|
-
path: Server path (for HTTP transports)
|
|
216
|
-
"""
|
|
200
|
+
def log_server_banner(server: FastMCP[Any]) -> None:
|
|
201
|
+
"""Creates and logs a formatted banner with server information and logo."""
|
|
217
202
|
|
|
218
203
|
# Create the logo text
|
|
219
204
|
# Use Text with no_wrap and markup disabled to preserve ANSI escape codes
|
|
@@ -228,39 +213,34 @@ def log_server_banner(
|
|
|
228
213
|
info_table.add_column(style="cyan", justify="left") # Label column
|
|
229
214
|
info_table.add_column(style="dim", justify="left") # Value column
|
|
230
215
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
display_transport = "HTTP"
|
|
234
|
-
case "sse":
|
|
235
|
-
display_transport = "SSE"
|
|
236
|
-
case "stdio":
|
|
237
|
-
display_transport = "STDIO"
|
|
238
|
-
|
|
239
|
-
info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
|
|
240
|
-
info_table.add_row("📦", "Transport:", display_transport)
|
|
241
|
-
|
|
242
|
-
# Show connection info based on transport
|
|
243
|
-
if transport in ("http", "streamable-http", "sse") and host and port:
|
|
244
|
-
server_url = f"http://{host}:{port}"
|
|
245
|
-
if path:
|
|
246
|
-
server_url += f"/{path.lstrip('/')}"
|
|
247
|
-
info_table.add_row("🔗", "Server URL:", server_url)
|
|
248
|
-
|
|
249
|
-
# Add documentation link
|
|
250
|
-
info_table.add_row("", "", "")
|
|
251
|
-
info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
|
|
252
|
-
info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
|
|
216
|
+
info_table.add_row("🖥", "Server:", Text(server.name, style="dim"))
|
|
217
|
+
info_table.add_row("🚀", "Deploy free:", "https://fastmcp.cloud")
|
|
253
218
|
|
|
254
219
|
# Create panel with logo, title, and information using Group
|
|
220
|
+
docs_url = Text("https://gofastmcp.com", style="dim")
|
|
255
221
|
panel_content = Group(
|
|
222
|
+
"",
|
|
256
223
|
Align.center(logo_text),
|
|
257
224
|
"",
|
|
258
|
-
Align.center(title_text),
|
|
259
225
|
"",
|
|
226
|
+
Align.center(title_text),
|
|
227
|
+
Align.center(docs_url),
|
|
260
228
|
"",
|
|
261
229
|
Align.center(info_table),
|
|
262
230
|
)
|
|
263
231
|
|
|
232
|
+
# v3 notice banner (shown below main panel)
|
|
233
|
+
v3_line1 = Text("✨ FastMCP 3.0 is coming!", style="bold")
|
|
234
|
+
v3_line2 = Text(
|
|
235
|
+
"Pin fastmcp<3 in production, then upgrade when you're ready.", style="dim"
|
|
236
|
+
)
|
|
237
|
+
v3_notice = Panel(
|
|
238
|
+
Group(Align.center(v3_line1), Align.center(v3_line2)),
|
|
239
|
+
border_style="blue",
|
|
240
|
+
padding=(0, 2),
|
|
241
|
+
width=80,
|
|
242
|
+
)
|
|
243
|
+
|
|
264
244
|
panel = Panel(
|
|
265
245
|
panel_content,
|
|
266
246
|
border_style="dim",
|
|
@@ -270,5 +250,5 @@ def log_server_banner(
|
|
|
270
250
|
)
|
|
271
251
|
|
|
272
252
|
console = Console(stderr=True)
|
|
273
|
-
# Center
|
|
274
|
-
console.print(Group("\n", Align.center(panel), "\n"))
|
|
253
|
+
# Center both panels
|
|
254
|
+
console.print(Group("\n", Align.center(panel), Align.center(v3_notice), "\n"))
|
fastmcp/utilities/json_schema.py
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]:
|
|
8
|
+
"""Resolve $ref at root level to meet MCP spec requirements.
|
|
9
|
+
|
|
10
|
+
MCP specification requires outputSchema to have "type": "object" at the root level.
|
|
11
|
+
When Pydantic generates schemas for self-referential models, it uses $ref at the
|
|
12
|
+
root level pointing to $defs. This function resolves such references by inlining
|
|
13
|
+
the referenced definition while preserving $defs for nested references.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
schema: JSON schema dict that may have $ref at root level
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A new schema dict with root-level $ref resolved, or the original schema
|
|
20
|
+
if no resolution is needed
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> schema = {
|
|
24
|
+
... "$defs": {"Node": {"type": "object", "properties": {...}}},
|
|
25
|
+
... "$ref": "#/$defs/Node"
|
|
26
|
+
... }
|
|
27
|
+
>>> resolved = resolve_root_ref(schema)
|
|
28
|
+
>>> # Result: {"type": "object", "properties": {...}, "$defs": {...}}
|
|
29
|
+
"""
|
|
30
|
+
# Only resolve if we have $ref at root level with $defs but no explicit type
|
|
31
|
+
if "$ref" in schema and "$defs" in schema and "type" not in schema:
|
|
32
|
+
ref = schema["$ref"]
|
|
33
|
+
# Only handle local $defs references
|
|
34
|
+
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
|
35
|
+
def_name = ref.split("/")[-1]
|
|
36
|
+
defs = schema["$defs"]
|
|
37
|
+
if def_name in defs:
|
|
38
|
+
# Create a new schema by copying the referenced definition
|
|
39
|
+
resolved = dict(defs[def_name])
|
|
40
|
+
# Preserve $defs for nested references (other fields may still use them)
|
|
41
|
+
resolved["$defs"] = defs
|
|
42
|
+
return resolved
|
|
43
|
+
return schema
|
|
4
44
|
|
|
5
45
|
|
|
6
46
|
def _prune_param(schema: dict, param: str) -> dict:
|
|
@@ -539,9 +539,9 @@ def extract_output_schema_from_responses(
|
|
|
539
539
|
# Replace $ref with the actual schema definition
|
|
540
540
|
output_schema = _replace_ref_with_defs(schema_definitions[schema_name])
|
|
541
541
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
542
|
+
if openapi_version and openapi_version.startswith("3"):
|
|
543
|
+
# Convert OpenAPI 3.x schema to JSON Schema format for proper handling
|
|
544
|
+
# of constructs like oneOf, anyOf, and nullable fields
|
|
545
545
|
from .json_schema_converter import convert_openapi_schema_to_json_schema
|
|
546
546
|
|
|
547
547
|
output_schema = convert_openapi_schema_to_json_schema(
|
|
@@ -570,7 +570,7 @@ def extract_output_schema_from_responses(
|
|
|
570
570
|
processed_defs[name] = _replace_ref_with_defs(schema)
|
|
571
571
|
|
|
572
572
|
# Convert OpenAPI schema definitions to JSON Schema format if needed
|
|
573
|
-
if openapi_version and openapi_version.startswith("3
|
|
573
|
+
if openapi_version and openapi_version.startswith("3"):
|
|
574
574
|
from .json_schema_converter import convert_openapi_schema_to_json_schema
|
|
575
575
|
|
|
576
576
|
for def_name in list(processed_defs.keys()):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.14.
|
|
3
|
+
Version: 2.14.2
|
|
4
4
|
Summary: The fast, Pythonic way to build MCP servers and clients.
|
|
5
5
|
Project-URL: Homepage, https://gofastmcp.com
|
|
6
6
|
Project-URL: Repository, https://github.com/jlowin/fastmcp
|
|
@@ -23,17 +23,19 @@ Requires-Dist: cyclopts>=4.0.0
|
|
|
23
23
|
Requires-Dist: exceptiongroup>=1.2.2
|
|
24
24
|
Requires-Dist: httpx>=0.28.1
|
|
25
25
|
Requires-Dist: jsonschema-path>=0.3.4
|
|
26
|
-
Requires-Dist: mcp
|
|
26
|
+
Requires-Dist: mcp<2.0,>=1.24.0
|
|
27
27
|
Requires-Dist: openapi-pydantic>=0.5.1
|
|
28
28
|
Requires-Dist: platformdirs>=4.0.0
|
|
29
29
|
Requires-Dist: py-key-value-aio[disk,keyring,memory]<0.4.0,>=0.3.0
|
|
30
30
|
Requires-Dist: pydantic[email]>=2.11.7
|
|
31
|
-
Requires-Dist: pydocket>=0.
|
|
31
|
+
Requires-Dist: pydocket>=0.16.3
|
|
32
32
|
Requires-Dist: pyperclip>=1.9.0
|
|
33
33
|
Requires-Dist: python-dotenv>=1.1.0
|
|
34
34
|
Requires-Dist: rich>=13.9.4
|
|
35
35
|
Requires-Dist: uvicorn>=0.35
|
|
36
36
|
Requires-Dist: websockets>=15.0.1
|
|
37
|
+
Provides-Extra: anthropic
|
|
38
|
+
Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
|
|
37
39
|
Provides-Extra: openai
|
|
38
40
|
Requires-Dist: openai>=1.102.0; extra == 'openai'
|
|
39
41
|
Description-Content-Type: text/markdown
|
|
@@ -73,6 +75,9 @@ Description-Content-Type: text/markdown
|
|
|
73
75
|
>
|
|
74
76
|
> **For production MCP applications, install FastMCP:** `pip install fastmcp`
|
|
75
77
|
|
|
78
|
+
> [!Important]
|
|
79
|
+
> FastMCP 3.0 is in development and may include breaking changes. To avoid unexpected issues, pin your dependency to v2: `fastmcp<3`
|
|
80
|
+
|
|
76
81
|
---
|
|
77
82
|
|
|
78
83
|
**FastMCP is the standard framework for building MCP applications**, providing the fastest path from idea to production.
|