fastmcp 2.2.6__py3-none-any.whl → 2.2.8__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.
@@ -97,15 +97,12 @@ class FunctionResource(Resource):
97
97
 
98
98
  if isinstance(result, Resource):
99
99
  return await result.read(context=context)
100
- if isinstance(result, bytes):
100
+ elif isinstance(result, bytes):
101
101
  return result
102
- if isinstance(result, str):
102
+ elif isinstance(result, str):
103
103
  return result
104
- try:
105
- return json.dumps(pydantic_core.to_jsonable_python(result))
106
- except (TypeError, pydantic_core.PydanticSerializationError):
107
- # If JSON serialization fails, try str()
108
- return str(result)
104
+ else:
105
+ return pydantic_core.to_json(result, fallback=str, indent=2).decode()
109
106
  except Exception as e:
110
107
  raise ValueError(f"Error reading resource {self.uri}: {e}")
111
108
 
fastmcp/server/context.py CHANGED
@@ -13,10 +13,12 @@ from mcp.types import (
13
13
  SamplingMessage,
14
14
  TextContent,
15
15
  )
16
- from pydantic import BaseModel
16
+ from pydantic import BaseModel, ConfigDict
17
17
  from pydantic.networks import AnyUrl
18
+ from starlette.requests import Request
18
19
 
19
20
  from fastmcp.server.server import FastMCP
21
+ from fastmcp.utilities.http import get_current_starlette_request
20
22
  from fastmcp.utilities.logging import get_logger
21
23
 
22
24
  logger = get_logger(__name__)
@@ -59,6 +61,8 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
59
61
  _request_context: RequestContext[ServerSessionT, LifespanContextT] | None
60
62
  _fastmcp: FastMCP | None
61
63
 
64
+ model_config = ConfigDict(arbitrary_types_allowed=True)
65
+
62
66
  def __init__(
63
67
  self,
64
68
  *,
@@ -222,3 +226,10 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
222
226
  )
223
227
 
224
228
  return result.content
229
+
230
+ def get_http_request(self) -> Request:
231
+ """Get the active starlette request."""
232
+ request = get_current_starlette_request()
233
+ if request is None:
234
+ raise ValueError("Request is not available outside a Starlette request")
235
+ return request
fastmcp/server/openapi.py CHANGED
@@ -5,19 +5,19 @@ from __future__ import annotations
5
5
  import enum
6
6
  import json
7
7
  import re
8
+ from collections.abc import Callable
8
9
  from dataclasses import dataclass
9
10
  from re import Pattern
10
11
  from typing import TYPE_CHECKING, Any, Literal
11
12
 
12
13
  import httpx
13
- from mcp.types import EmbeddedResource, ImageContent, TextContent
14
+ from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
14
15
  from pydantic.networks import AnyUrl
15
16
 
16
17
  from fastmcp.resources import Resource, ResourceTemplate
17
18
  from fastmcp.server.server import FastMCP
18
19
  from fastmcp.tools.tool import Tool, _convert_to_content
19
20
  from fastmcp.utilities import openapi
20
- from fastmcp.utilities.func_metadata import func_metadata
21
21
  from fastmcp.utilities.logging import get_logger
22
22
  from fastmcp.utilities.openapi import (
23
23
  _combine_schemas,
@@ -122,20 +122,20 @@ class OpenAPITool(Tool):
122
122
  name: str,
123
123
  description: str,
124
124
  parameters: dict[str, Any],
125
- fn_metadata: Any,
126
- is_async: bool = True,
127
125
  tags: set[str] = set(),
128
126
  timeout: float | None = None,
127
+ annotations: ToolAnnotations | None = None,
128
+ serializer: Callable[[Any], str] | None = None,
129
129
  ):
130
130
  super().__init__(
131
131
  name=name,
132
132
  description=description,
133
133
  parameters=parameters,
134
134
  fn=self._execute_request, # We'll use an instance method instead of a global function
135
- fn_metadata=fn_metadata,
136
- is_async=is_async,
137
135
  context_kwarg="context", # Default context keyword argument
138
136
  tags=tags,
137
+ annotations=annotations,
138
+ serializer=serializer,
139
139
  )
140
140
  self._client = client
141
141
  self._route = route
@@ -534,10 +534,12 @@ class FastMCPOpenAPI(FastMCP):
534
534
  or f"Executes {route.method} {route.path}"
535
535
  )
536
536
 
537
- # Format enhanced description
537
+ # Format enhanced description with parameters and request body
538
538
  enhanced_description = format_description_with_responses(
539
539
  base_description=base_description,
540
540
  responses=route.responses,
541
+ parameters=route.parameters,
542
+ request_body=route.request_body,
541
543
  )
542
544
 
543
545
  tool = OpenAPITool(
@@ -546,8 +548,6 @@ class FastMCPOpenAPI(FastMCP):
546
548
  name=tool_name,
547
549
  description=enhanced_description,
548
550
  parameters=combined_schema,
549
- fn_metadata=func_metadata(_openapi_passthrough),
550
- is_async=True,
551
551
  tags=set(route.tags or []),
552
552
  timeout=self._timeout,
553
553
  )
@@ -565,10 +565,12 @@ class FastMCPOpenAPI(FastMCP):
565
565
  route.description or route.summary or f"Represents {route.path}"
566
566
  )
567
567
 
568
- # Format enhanced description
568
+ # Format enhanced description with parameters and request body
569
569
  enhanced_description = format_description_with_responses(
570
570
  base_description=base_description,
571
571
  responses=route.responses,
572
+ parameters=route.parameters,
573
+ request_body=route.request_body,
572
574
  )
573
575
 
574
576
  resource = OpenAPIResource(
@@ -600,16 +602,30 @@ class FastMCPOpenAPI(FastMCP):
600
602
  route.description or route.summary or f"Template for {route.path}"
601
603
  )
602
604
 
603
- # Format enhanced description
605
+ # Format enhanced description with parameters and request body
604
606
  enhanced_description = format_description_with_responses(
605
607
  base_description=base_description,
606
608
  responses=route.responses,
609
+ parameters=route.parameters,
610
+ request_body=route.request_body,
607
611
  )
608
612
 
609
613
  template_params_schema = {
610
614
  "type": "object",
611
615
  "properties": {
612
- p.name: p.schema_ for p in route.parameters if p.location == "path"
616
+ p.name: {
617
+ **(p.schema_.copy() if isinstance(p.schema_, dict) else {}),
618
+ **(
619
+ {"description": p.description}
620
+ if p.description
621
+ and not (
622
+ isinstance(p.schema_, dict) and "description" in p.schema_
623
+ )
624
+ else {}
625
+ ),
626
+ }
627
+ for p in route.parameters
628
+ if p.location == "path"
613
629
  },
614
630
  "required": [
615
631
  p.name for p in route.parameters if p.location == "path" and p.required
fastmcp/server/proxy.py CHANGED
@@ -19,12 +19,11 @@ from pydantic.networks import AnyUrl
19
19
 
20
20
  from fastmcp.client import Client
21
21
  from fastmcp.exceptions import NotFoundError
22
- from fastmcp.prompts import Message, Prompt
22
+ from fastmcp.prompts import Prompt, PromptMessage
23
23
  from fastmcp.resources import Resource, ResourceTemplate
24
24
  from fastmcp.server.context import Context
25
25
  from fastmcp.server.server import FastMCP
26
26
  from fastmcp.tools.tool import Tool
27
- from fastmcp.utilities.func_metadata import func_metadata
28
27
  from fastmcp.utilities.logging import get_logger
29
28
 
30
29
  if TYPE_CHECKING:
@@ -53,8 +52,6 @@ class ProxyTool(Tool):
53
52
  description=tool.description,
54
53
  parameters=tool.inputSchema,
55
54
  fn=_proxy_passthrough,
56
- fn_metadata=func_metadata(_proxy_passthrough),
57
- is_async=True,
58
55
  )
59
56
 
60
57
  async def run(
@@ -65,8 +62,9 @@ class ProxyTool(Tool):
65
62
  # the client context manager will swallow any exceptions inside a TaskGroup
66
63
  # so we return the raw result and raise an exception ourselves
67
64
  async with self._client:
68
- result = await self._client.call_tool(
69
- self.name, arguments, _return_raw_result=True
65
+ result = await self._client.call_tool_mcp(
66
+ name=self.name,
67
+ arguments=arguments,
70
68
  )
71
69
  if result.isError:
72
70
  raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
@@ -177,10 +175,10 @@ class ProxyPrompt(Prompt):
177
175
  self,
178
176
  arguments: dict[str, Any],
179
177
  context: Context[ServerSessionT, LifespanContextT] | None = None,
180
- ) -> list[Message]:
178
+ ) -> list[PromptMessage]:
181
179
  async with self._client:
182
180
  result = await self._client.get_prompt(self.name, arguments)
183
- return [Message(role=m.role, content=m.content) for m in result]
181
+ return result.messages
184
182
 
185
183
 
186
184
  class FastMCPProxy(FastMCP):
@@ -293,4 +291,4 @@ class FastMCPProxy(FastMCP):
293
291
  except NotFoundError:
294
292
  async with self.client:
295
293
  result = await self.client.get_prompt(name, arguments)
296
- return GetPromptResult(messages=result)
294
+ return result
fastmcp/server/server.py CHANGED
@@ -14,8 +14,13 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
14
14
 
15
15
  import anyio
16
16
  import httpx
17
- import pydantic_core
18
17
  import uvicorn
18
+ from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
19
+ from mcp.server.auth.middleware.bearer_auth import (
20
+ BearerAuthBackend,
21
+ RequireAuthMiddleware,
22
+ )
23
+ from mcp.server.auth.provider import OAuthAuthorizationServerProvider
19
24
  from mcp.server.lowlevel.helper_types import ReadResourceContents
20
25
  from mcp.server.lowlevel.server import LifespanResultT
21
26
  from mcp.server.lowlevel.server import Server as MCPServer
@@ -28,6 +33,7 @@ from mcp.types import (
28
33
  GetPromptResult,
29
34
  ImageContent,
30
35
  TextContent,
36
+ ToolAnnotations,
31
37
  )
32
38
  from mcp.types import Prompt as MCPPrompt
33
39
  from mcp.types import Resource as MCPResource
@@ -35,8 +41,12 @@ from mcp.types import ResourceTemplate as MCPResourceTemplate
35
41
  from mcp.types import Tool as MCPTool
36
42
  from pydantic.networks import AnyUrl
37
43
  from starlette.applications import Starlette
44
+ from starlette.middleware import Middleware
45
+ from starlette.middleware.authentication import AuthenticationMiddleware
38
46
  from starlette.requests import Request
47
+ from starlette.responses import Response
39
48
  from starlette.routing import Mount, Route
49
+ from starlette.types import Receive, Scope, Send
40
50
 
41
51
  import fastmcp
42
52
  import fastmcp.settings
@@ -48,6 +58,7 @@ from fastmcp.resources.template import ResourceTemplate
48
58
  from fastmcp.tools import ToolManager
49
59
  from fastmcp.tools.tool import Tool
50
60
  from fastmcp.utilities.decorators import DecoratedFunction
61
+ from fastmcp.utilities.http import RequestMiddleware
51
62
  from fastmcp.utilities.logging import configure_logging, get_logger
52
63
 
53
64
  if TYPE_CHECKING:
@@ -183,6 +194,8 @@ class FastMCP(Generic[LifespanResultT]):
183
194
  self,
184
195
  name: str | None = None,
185
196
  instructions: str | None = None,
197
+ auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any]
198
+ | None = None,
186
199
  lifespan: (
187
200
  Callable[
188
201
  [FastMCP[LifespanResultT]],
@@ -191,6 +204,7 @@ class FastMCP(Generic[LifespanResultT]):
191
204
  | None
192
205
  ) = None,
193
206
  tags: set[str] | None = None,
207
+ tool_serializer: Callable[[Any], str] | None = None,
194
208
  **settings: Any,
195
209
  ):
196
210
  self.tags: set[str] = tags or set()
@@ -204,7 +218,10 @@ class FastMCP(Generic[LifespanResultT]):
204
218
  self._mounted_servers: dict[str, MountedServer] = {}
205
219
 
206
220
  if lifespan is None:
221
+ self._has_lifespan = False
207
222
  lifespan = default_lifespan
223
+ else:
224
+ self._has_lifespan = True
208
225
 
209
226
  self._mcp_server = MCPServer[LifespanResultT](
210
227
  name=name or "FastMCP",
@@ -212,7 +229,8 @@ class FastMCP(Generic[LifespanResultT]):
212
229
  lifespan=_lifespan_wrapper(self, lifespan),
213
230
  )
214
231
  self._tool_manager = ToolManager(
215
- duplicate_behavior=self.settings.on_duplicate_tools
232
+ duplicate_behavior=self.settings.on_duplicate_tools,
233
+ serializer=tool_serializer,
216
234
  )
217
235
  self._resource_manager = ResourceManager(
218
236
  duplicate_behavior=self.settings.on_duplicate_resources
@@ -220,6 +238,15 @@ class FastMCP(Generic[LifespanResultT]):
220
238
  self._prompt_manager = PromptManager(
221
239
  duplicate_behavior=self.settings.on_duplicate_prompts
222
240
  )
241
+
242
+ if (self.settings.auth is not None) != (auth_server_provider is not None):
243
+ # TODO: after we support separate authorization servers (see
244
+ raise ValueError(
245
+ "settings.auth must be specified if and only if auth_server_provider "
246
+ "is specified"
247
+ )
248
+ self._auth_server_provider = auth_server_provider
249
+ self._custom_starlette_routes: list[Route] = []
223
250
  self.dependencies = self.settings.dependencies
224
251
 
225
252
  # Set up MCP protocol handlers
@@ -339,6 +366,50 @@ class FastMCP(Generic[LifespanResultT]):
339
366
  self._cache.set("prompts", prompts)
340
367
  return prompts
341
368
 
369
+ def custom_route(
370
+ self,
371
+ path: str,
372
+ methods: list[str],
373
+ name: str | None = None,
374
+ include_in_schema: bool = True,
375
+ ):
376
+ """
377
+ Decorator to register a custom HTTP route on the FastMCP server.
378
+
379
+ Allows adding arbitrary HTTP endpoints outside the standard MCP protocol,
380
+ which can be useful for OAuth callbacks, health checks, or admin APIs.
381
+ The handler function must be an async function that accepts a Starlette
382
+ Request and returns a Response.
383
+
384
+ Args:
385
+ path: URL path for the route (e.g., "/oauth/callback")
386
+ methods: List of HTTP methods to support (e.g., ["GET", "POST"])
387
+ name: Optional name for the route (to reference this route with
388
+ Starlette's reverse URL lookup feature)
389
+ include_in_schema: Whether to include in OpenAPI schema, defaults to True
390
+
391
+ Example:
392
+ @server.custom_route("/health", methods=["GET"])
393
+ async def health_check(request: Request) -> Response:
394
+ return JSONResponse({"status": "ok"})
395
+ """
396
+
397
+ def decorator(
398
+ func: Callable[[Request], Awaitable[Response]],
399
+ ) -> Callable[[Request], Awaitable[Response]]:
400
+ self._custom_starlette_routes.append(
401
+ Route(
402
+ path,
403
+ endpoint=func,
404
+ methods=methods,
405
+ name=name,
406
+ include_in_schema=include_in_schema,
407
+ )
408
+ )
409
+ return func
410
+
411
+ return decorator
412
+
342
413
  async def _mcp_list_tools(self) -> list[MCPTool]:
343
414
  """
344
415
  List all available tools, in the format expected by the low-level MCP
@@ -432,10 +503,10 @@ class FastMCP(Generic[LifespanResultT]):
432
503
  """
433
504
  if self._prompt_manager.has_prompt(name):
434
505
  context = self.get_context()
435
- messages = await self._prompt_manager.render_prompt(
506
+ prompt_result = await self._prompt_manager.render_prompt(
436
507
  name, arguments=arguments or {}, context=context
437
508
  )
438
- return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
509
+ return prompt_result
439
510
  else:
440
511
  for server in self._mounted_servers.values():
441
512
  if server.match_prompt(name):
@@ -450,6 +521,7 @@ class FastMCP(Generic[LifespanResultT]):
450
521
  name: str | None = None,
451
522
  description: str | None = None,
452
523
  tags: set[str] | None = None,
524
+ annotations: ToolAnnotations | dict[str, Any] | None = None,
453
525
  ) -> None:
454
526
  """Add a tool to the server.
455
527
 
@@ -461,9 +533,17 @@ class FastMCP(Generic[LifespanResultT]):
461
533
  name: Optional name for the tool (defaults to function name)
462
534
  description: Optional description of what the tool does
463
535
  tags: Optional set of tags for categorizing the tool
536
+ annotations: Optional annotations about the tool's behavior
464
537
  """
538
+ if isinstance(annotations, dict):
539
+ annotations = ToolAnnotations(**annotations)
540
+
465
541
  self._tool_manager.add_tool_from_fn(
466
- fn, name=name, description=description, tags=tags
542
+ fn,
543
+ name=name,
544
+ description=description,
545
+ tags=tags,
546
+ annotations=annotations,
467
547
  )
468
548
  self._cache.clear()
469
549
 
@@ -472,6 +552,7 @@ class FastMCP(Generic[LifespanResultT]):
472
552
  name: str | None = None,
473
553
  description: str | None = None,
474
554
  tags: set[str] | None = None,
555
+ annotations: ToolAnnotations | dict[str, Any] | None = None,
475
556
  ) -> Callable[[AnyFunction], AnyFunction]:
476
557
  """Decorator to register a tool.
477
558
 
@@ -483,6 +564,7 @@ class FastMCP(Generic[LifespanResultT]):
483
564
  name: Optional name for the tool (defaults to function name)
484
565
  description: Optional description of what the tool does
485
566
  tags: Optional set of tags for categorizing the tool
567
+ annotations: Optional annotations about the tool's behavior
486
568
 
487
569
  Example:
488
570
  @server.tool()
@@ -508,7 +590,13 @@ class FastMCP(Generic[LifespanResultT]):
508
590
  )
509
591
 
510
592
  def decorator(fn: AnyFunction) -> AnyFunction:
511
- self.add_tool(fn, name=name, description=description, tags=tags)
593
+ self.add_tool(
594
+ fn,
595
+ name=name,
596
+ description=description,
597
+ tags=tags,
598
+ annotations=annotations,
599
+ )
512
600
  return fn
513
601
 
514
602
  return decorator
@@ -732,41 +820,125 @@ class FastMCP(Generic[LifespanResultT]):
732
820
  host: str | None = None,
733
821
  port: int | None = None,
734
822
  log_level: str | None = None,
823
+ uvicorn_config: dict | None = None,
735
824
  ) -> None:
736
825
  """Run the server using SSE transport."""
737
- starlette_app = self.sse_app()
826
+ uvicorn_config = uvicorn_config or {}
827
+ # the SSE app hangs even when a signal is sent, so we disable the timeout to make it possible to close immediately.
828
+ # see https://github.com/jlowin/fastmcp/issues/296
829
+ uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
830
+ app = RequestMiddleware(self.sse_app())
738
831
 
739
832
  config = uvicorn.Config(
740
- starlette_app,
833
+ app,
741
834
  host=host or self.settings.host,
742
835
  port=port or self.settings.port,
743
836
  log_level=log_level or self.settings.log_level.lower(),
837
+ **uvicorn_config,
744
838
  )
745
839
  server = uvicorn.Server(config)
746
840
  await server.serve()
747
841
 
748
842
  def sse_app(self) -> Starlette:
749
843
  """Return an instance of the SSE server app."""
844
+ from starlette.middleware import Middleware
845
+ from starlette.routing import Mount, Route
846
+
847
+ # Set up auth context and dependencies
848
+
750
849
  sse = SseServerTransport(self.settings.message_path)
751
850
 
752
- async def handle_sse(request: Request) -> None:
851
+ async def handle_sse(scope: Scope, receive: Receive, send: Send):
852
+ # Add client ID from auth context into request context if available
853
+
753
854
  async with sse.connect_sse(
754
- request.scope,
755
- request.receive,
756
- request._send, # type: ignore[reportPrivateUsage]
855
+ scope,
856
+ receive,
857
+ send,
757
858
  ) as streams:
758
859
  await self._mcp_server.run(
759
860
  streams[0],
760
861
  streams[1],
761
862
  self._mcp_server.create_initialization_options(),
762
863
  )
864
+ return Response()
865
+
866
+ # Create routes
867
+ routes: list[Route | Mount] = []
868
+ middleware: list[Middleware] = []
869
+ required_scopes = []
870
+
871
+ # Add auth endpoints if auth provider is configured
872
+ if self._auth_server_provider:
873
+ assert self.settings.auth
874
+ from mcp.server.auth.routes import create_auth_routes
875
+
876
+ required_scopes = self.settings.auth.required_scopes or []
877
+
878
+ middleware = [
879
+ # extract auth info from request (but do not require it)
880
+ Middleware(
881
+ AuthenticationMiddleware,
882
+ backend=BearerAuthBackend(
883
+ provider=self._auth_server_provider,
884
+ ),
885
+ ),
886
+ # Add the auth context middleware to store
887
+ # authenticated user in a contextvar
888
+ Middleware(AuthContextMiddleware),
889
+ ]
890
+ routes.extend(
891
+ create_auth_routes(
892
+ provider=self._auth_server_provider,
893
+ issuer_url=self.settings.auth.issuer_url,
894
+ service_documentation_url=self.settings.auth.service_documentation_url,
895
+ client_registration_options=self.settings.auth.client_registration_options,
896
+ revocation_options=self.settings.auth.revocation_options,
897
+ )
898
+ )
763
899
 
900
+ # When auth is not configured, we shouldn't require auth
901
+ if self._auth_server_provider:
902
+ # Auth is enabled, wrap the endpoints with RequireAuthMiddleware
903
+ routes.append(
904
+ Route(
905
+ self.settings.sse_path,
906
+ endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
907
+ methods=["GET"],
908
+ )
909
+ )
910
+ routes.append(
911
+ Mount(
912
+ self.settings.message_path,
913
+ app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
914
+ )
915
+ )
916
+ else:
917
+ # Auth is disabled, no need for RequireAuthMiddleware
918
+ # Since handle_sse is an ASGI app, we need to create a compatible endpoint
919
+ async def sse_endpoint(request: Request) -> None:
920
+ # Convert the Starlette request to ASGI parameters
921
+ await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
922
+
923
+ routes.append(
924
+ Route(
925
+ self.settings.sse_path,
926
+ endpoint=sse_endpoint,
927
+ methods=["GET"],
928
+ )
929
+ )
930
+ routes.append(
931
+ Mount(
932
+ self.settings.message_path,
933
+ app=sse.handle_post_message,
934
+ )
935
+ )
936
+ # mount these routes last, so they have the lowest route matching precedence
937
+ routes.extend(self._custom_starlette_routes)
938
+
939
+ # Create Starlette app with routes and middleware
764
940
  return Starlette(
765
- debug=self.settings.debug,
766
- routes=[
767
- Route(self.settings.sse_path, endpoint=handle_sse),
768
- Mount(self.settings.message_path, app=sse.handle_post_message),
769
- ],
941
+ debug=self.settings.debug, routes=routes, middleware=middleware
770
942
  )
771
943
 
772
944
  def mount(
@@ -776,10 +948,62 @@ class FastMCP(Generic[LifespanResultT]):
776
948
  tool_separator: str | None = None,
777
949
  resource_separator: str | None = None,
778
950
  prompt_separator: str | None = None,
951
+ as_proxy: bool | None = None,
779
952
  ) -> None:
953
+ """Mount another FastMCP server on this server with the given prefix.
954
+
955
+ Unlike importing (with import_server), mounting establishes a dynamic connection
956
+ between servers. When a client interacts with a mounted server's objects through
957
+ the parent server, requests are forwarded to the mounted server in real-time.
958
+ This means changes to the mounted server are immediately reflected when accessed
959
+ through the parent.
960
+
961
+ When a server is mounted:
962
+ - Tools from the mounted server are accessible with prefixed names using the tool_separator.
963
+ Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
964
+ - Resources are accessible with prefixed URIs using the resource_separator.
965
+ Example: If server has a resource with URI "weather://forecast", it will be available as
966
+ "prefix+weather://forecast".
967
+ - Templates are accessible with prefixed URI templates using the resource_separator.
968
+ Example: If server has a template with URI "weather://location/{id}", it will be available
969
+ as "prefix+weather://location/{id}".
970
+ - Prompts are accessible with prefixed names using the prompt_separator.
971
+ Example: If server has a prompt named "weather_prompt", it will be available as
972
+ "prefix_weather_prompt".
973
+
974
+ There are two modes for mounting servers:
975
+ 1. Direct mounting (default when server has no custom lifespan): The parent server
976
+ directly accesses the mounted server's objects in-memory for better performance.
977
+ In this mode, no client lifecycle events occur on the mounted server, including
978
+ lifespan execution.
979
+
980
+ 2. Proxy mounting (default when server has a custom lifespan): The parent server
981
+ treats the mounted server as a separate entity and communicates with it via a
982
+ Client transport. This preserves all client-facing behaviors, including lifespan
983
+ execution, but with slightly higher overhead.
984
+
985
+ Args:
986
+ prefix: Prefix to use for the mounted server's objects.
987
+ server: The FastMCP server to mount.
988
+ tool_separator: Separator character for tool names (defaults to "_").
989
+ resource_separator: Separator character for resource URIs (defaults to "+").
990
+ prompt_separator: Separator character for prompt names (defaults to "_").
991
+ as_proxy: Whether to treat the mounted server as a proxy. If None (default),
992
+ automatically determined based on whether the server has a custom lifespan
993
+ (True if it has a custom lifespan, False otherwise).
780
994
  """
781
- Mount another FastMCP server on a given prefix.
782
- """
995
+ from fastmcp import Client
996
+ from fastmcp.client.transports import FastMCPTransport
997
+ from fastmcp.server.proxy import FastMCPProxy
998
+
999
+ # if as_proxy is not specified and the server has a custom lifespan,
1000
+ # we should treat it as a proxy
1001
+ if as_proxy is None:
1002
+ as_proxy = server._has_lifespan
1003
+
1004
+ if as_proxy and not isinstance(server, FastMCPProxy):
1005
+ server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
1006
+
783
1007
  mounted_server = MountedServer(
784
1008
  server=server,
785
1009
  prefix=prefix,
fastmcp/settings.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Literal
4
4
 
5
+ from mcp.server.auth.settings import AuthSettings
5
6
  from pydantic import Field
6
7
  from pydantic_settings import BaseSettings, SettingsConfigDict
7
8
 
@@ -20,6 +21,8 @@ class Settings(BaseSettings):
20
21
  env_prefix="FASTMCP_",
21
22
  env_file=".env",
22
23
  extra="ignore",
24
+ env_nested_delimiter="__",
25
+ nested_model_default_partial_update=True,
23
26
  )
24
27
 
25
28
  test_mode: bool = False
@@ -37,6 +40,8 @@ class ServerSettings(BaseSettings):
37
40
  env_prefix="FASTMCP_SERVER_",
38
41
  env_file=".env",
39
42
  extra="ignore",
43
+ env_nested_delimiter="__",
44
+ nested_model_default_partial_update=True,
40
45
  )
41
46
 
42
47
  log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
@@ -65,6 +70,8 @@ class ServerSettings(BaseSettings):
65
70
  # cache settings (for checking mounted servers)
66
71
  cache_expiration_seconds: float = 0
67
72
 
73
+ auth: AuthSettings | None = None
74
+
68
75
 
69
76
  class ClientSettings(BaseSettings):
70
77
  """FastMCP client settings."""