4444from mcp .server .lowlevel .server import LifespanResultT
4545from mcp .server .lowlevel .server import Server as MCPServer
4646from mcp .server .lowlevel .server import lifespan as default_lifespan
47+ from mcp .server .message_queue import MessageDispatch
4748from mcp .server .session import ServerSession , ServerSessionT
4849from mcp .server .sse import SseServerTransport
4950from mcp .server .stdio import stdio_server
@@ -87,9 +88,15 @@ class Settings(BaseSettings, Generic[LifespanResultT]):
8788 # HTTP settings
8889 host : str = "0.0.0.0"
8990 port : int = 8000
91+ mount_path : str = "/" # Mount path (e.g. "/github", defaults to root path)
9092 sse_path : str = "/sse"
9193 message_path : str = "/messages/"
9294
95+ # SSE message queue settings
96+ message_dispatch : MessageDispatch | None = Field (
97+ None , description = "Custom message dispatch instance"
98+ )
99+
93100 # resource settings
94101 warn_on_duplicate_resources : bool = True
95102
@@ -178,11 +185,16 @@ def name(self) -> str:
178185 def instructions (self ) -> str | None :
179186 return self ._mcp_server .instructions
180187
181- def run (self , transport : Literal ["stdio" , "sse" ] = "stdio" ) -> None :
188+ def run (
189+ self ,
190+ transport : Literal ["stdio" , "sse" ] = "stdio" ,
191+ mount_path : str | None = None ,
192+ ) -> None :
182193 """Run the FastMCP server. Note this is a synchronous function.
183194
184195 Args:
185196 transport: Transport protocol to use ("stdio" or "sse")
197+ mount_path: Optional mount path for SSE transport
186198 """
187199 TRANSPORTS = Literal ["stdio" , "sse" ]
188200 if transport not in TRANSPORTS .__args__ : # type: ignore
@@ -191,7 +203,7 @@ def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None:
191203 if transport == "stdio" :
192204 anyio .run (self .run_stdio_async )
193205 else : # transport == "sse"
194- anyio .run (self .run_sse_async )
206+ anyio .run (lambda : self .run_sse_async ( mount_path ) )
195207
196208 def _setup_handlers (self ) -> None :
197209 """Set up core MCP protocol handlers."""
@@ -552,11 +564,11 @@ async def run_stdio_async(self) -> None:
552564 self ._mcp_server .create_initialization_options (),
553565 )
554566
555- async def run_sse_async (self ) -> None :
567+ async def run_sse_async (self , mount_path : str | None = None ) -> None :
556568 """Run the server using SSE transport."""
557569 import uvicorn
558570
559- starlette_app = self .sse_app ()
571+ starlette_app = self .sse_app (mount_path )
560572
561573 config = uvicorn .Config (
562574 starlette_app ,
@@ -567,14 +579,59 @@ async def run_sse_async(self) -> None:
567579 server = uvicorn .Server (config )
568580 await server .serve ()
569581
570- def sse_app (self ) -> Starlette :
582+ def _normalize_path (self , mount_path : str , endpoint : str ) -> str :
583+ """
584+ Combine mount path and endpoint to return a normalized path.
585+
586+ Args:
587+ mount_path: The mount path (e.g. "/github" or "/")
588+ endpoint: The endpoint path (e.g. "/messages/")
589+
590+ Returns:
591+ Normalized path (e.g. "/github/messages/")
592+ """
593+ # Special case: root path
594+ if mount_path == "/" :
595+ return endpoint
596+
597+ # Remove trailing slash from mount path
598+ if mount_path .endswith ("/" ):
599+ mount_path = mount_path [:- 1 ]
600+
601+ # Ensure endpoint starts with slash
602+ if not endpoint .startswith ("/" ):
603+ endpoint = "/" + endpoint
604+
605+ # Combine paths
606+ return mount_path + endpoint
607+
608+ def sse_app (self , mount_path : str | None = None ) -> Starlette :
571609 """Return an instance of the SSE server app."""
610+ message_dispatch = self .settings .message_dispatch
611+ if message_dispatch is None :
612+ from mcp .server .message_queue import InMemoryMessageDispatch
613+
614+ message_dispatch = InMemoryMessageDispatch ()
615+ logger .info ("Using default in-memory message dispatch" )
616+
572617 from starlette .middleware import Middleware
573618 from starlette .routing import Mount , Route
574619
620+ # Update mount_path in settings if provided
621+ if mount_path is not None :
622+ self .settings .mount_path = mount_path
623+
624+ # Create normalized endpoint considering the mount path
625+ normalized_message_endpoint = self ._normalize_path (
626+ self .settings .mount_path , self .settings .message_path
627+ )
628+
575629 # Set up auth context and dependencies
576630
577- sse = SseServerTransport (self .settings .message_path )
631+ sse = SseServerTransport (
632+ normalized_message_endpoint ,
633+ message_dispatch = message_dispatch
634+ )
578635
579636 async def handle_sse (scope : Scope , receive : Receive , send : Send ):
580637 # Add client ID from auth context into request context if available
@@ -589,7 +646,14 @@ async def handle_sse(scope: Scope, receive: Receive, send: Send):
589646 streams [1 ],
590647 self ._mcp_server .create_initialization_options (),
591648 )
592- return Response ()
649+ return Response ()
650+
651+ @asynccontextmanager
652+ async def lifespan (app : Starlette ):
653+ try :
654+ yield
655+ finally :
656+ await message_dispatch .close ()
593657
594658 # Create routes
595659 routes : list [Route | Mount ] = []
@@ -666,7 +730,10 @@ async def sse_endpoint(request: Request) -> None:
666730
667731 # Create Starlette app with routes and middleware
668732 return Starlette (
669- debug = self .settings .debug , routes = routes , middleware = middleware
733+ debug = self .settings .debug ,
734+ routes = routes ,
735+ middleware = middleware ,
736+ lifespan = lifespan ,
670737 )
671738
672739 async def list_prompts (self ) -> list [MCPPrompt ]:
0 commit comments