how to manage multi streamable http server lifespan
Describe the bug I write a multi streamable http server example according to your readme doc, but only the mcp server which pass the session_manager.run() to FastAPI works right
To Reproduce mcp python sdk ver 1.8.1
mcp server code
from fastapi import FastAPI
import uvicorn
from mcp.server.fastmcp import FastMCP
echo_mcp = FastMCP(name="EchoServer", stateless_http=True)
@echo_mcp.tool(description="A simple echo tool")
def echo(message: str) -> str:
return f"Echo: {message}"
math_mcp = FastMCP(name="MathServer", stateless_http=True)
@math_mcp.tool(description="A simple add tool")
def add_two(m: int, n: int) -> int:
return m + n
app = FastAPI(lifespan=lambda app: echo_mcp.session_manager.run())
app.mount("/echo", echo_mcp.streamable_http_app())
app.mount("/math", math_mcp.streamable_http_app())
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)
mcp client code
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
server_url = "http://127.0.0.1:8000/math/mcp"
async with streamablehttp_client(server_url) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
tools = tools.tools
print(f"found {len(tools)} tools:")
if __name__ == "__main__":
asyncio.run(main())
Expected behavior Every mcp server works right.
Error Stack
/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/bin/python /Users/karsalina/PycharmProjects/playGround/script/http_server.py
INFO: Started server process [44368]
INFO: Waiting for application startup.
[05/14/25 16:21:02] INFO StreamableHTTP streamable_http_manager.py:109
session manager
started
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: 127.0.0.1:49685 - "POST /math/mcp HTTP/1.1" 307 Temporary Redirect
INFO: 127.0.0.1:49687 - "POST /math/mcp/ HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/uvicorn/protocols/http/httptools_impl.py", line 411, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 69, in __call__
return await self.app(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
await super().__call__(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/applications.py", line 112, in __call__
await self.middleware_stack(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/middleware/errors.py", line 187, in __call__
raise exc
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/middleware/errors.py", line 165, in __call__
await self.app(scope, receive, _send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
raise exc
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/routing.py", line 714, in __call__
await self.middleware_stack(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/routing.py", line 734, in app
await route.handle(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/routing.py", line 460, in handle
await self.app(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/applications.py", line 112, in __call__
await self.middleware_stack(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/middleware/errors.py", line 187, in __call__
raise exc
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/middleware/errors.py", line 165, in __call__
await self.app(scope, receive, _send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
raise exc
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/routing.py", line 714, in __call__
await self.middleware_stack(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/routing.py", line 734, in app
await route.handle(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/starlette/routing.py", line 460, in handle
await self.app(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/mcp/server/fastmcp/server.py", line 782, in handle_streamable_http
await self.session_manager.handle_request(scope, receive, send)
File "/Users/karsalina/Library/Caches/pypoetry/virtualenvs/playground-p-gXMEVB-py3.10/lib/python3.10/site-packages/mcp/server/streamable_http_manager.py", line 137, in handle_request
raise RuntimeError("Task group is not initialized. Make sure to use run().")
RuntimeError: Task group is not initialized. Make sure to use run().
I have the same problem
thank you for reporting this! Updated the example in readme
its failing for me too for basic example with mcp streamable HTTP transport, what is the solution?
thank you for reporting this! Updated the example in readme
Based on your example, if I want to access the database in an mcp server and I want to use lifespan, but I don't know exactly how to write it
got the same problem!
by the way, anybody knows how to merge FastAPI's lifespan and FastMCP's lifespan?
blow is my test code:
`python from fastmcp import FastMCP from starlette.routing import Mount
from contextlib import asynccontextmanager
mcp = FastMCP(name="MyServer") mcp_app = mcp.http_app(path='/mcp')
from fastapi import FastAPI
@asynccontextmanager async def main_app_lifespan(app: FastAPI): # do some main app's init work pass
app = FastAPI(lifespan=mcp_app.lifespan, routes=[Mount('/mcp_http_kn', app=mcp_app)])
@app.get("/test") async def test(): return 'test'
@mcp.tool() def greet(name: str) -> str: """Greet a user by name.""" return f"Hello, {name}!"
if name == "main": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
`
I reported the same issue in a different usage context: https://github.com/modelcontextprotocol/python-sdk/issues/838
In my opinion, this appears to be an issue with the Python SDK, not a documentation issue.
The FastMCP class should be able to handle its intended functionality without additional setup, or it should fail early with a clear error message if additional configuration is needed.
its failing for me too for basic example with mcp streamable HTTP transport, what is the solution?
Please can you share the example and how you are running it?
@bygzyz Please see: https://www.reddit.com/r/mcp/comments/1kizgw2/fastapi_fastmcp_integration_question/
I have the same problem!
starlette mount multi mcp server. sse can work, but streamable http not.
RuntimeError: FastMCP's StreamableHTTPSessionManager task group was not initialized. This commonly occurs when the FastMCP application's lifespan is not passed to the parent ASGI application (e.g., FastAPI or Starlette). Please ensure you are setting lifespan=mcp_app.lifespan in your parent app's constructor, where mcp_app is the application instance returned by fastmcp_instance.http_app(). \nFor more details, see the FastMCP ASGI integration documentation: https://gofastmcp.com/deployment/asgi\nOriginal error: Task group is not initialized. Make sure to use run().
app = Starlette(
routes=[Route("/health", health_check)],
lifespan=combined_lifespan(proxy_apps)
)
def combined_lifespan(proxy_apps):
async def lifespan(app):
async with contextlib.AsyncExitStack() as stack:
for p in proxy_apps:
await stack.enter_async_context(p["asgi_app"].lifespan(app))
yield
return lifespan
Thanks @ZhangHaoWeb. I have been using a similar implementation for mounting a new mcp app to an existing FastAPI server. I am looking for ways to make it easy for other service owners to do this automatically - something in a simple function call like: mount_mcp_app(current_app, new_app)
Is this even possible? can we rewrite the lifespan of an already created mcp app? If so, this function can take the currentapp and newapp's lifespan, combine them and reassign them to the old app.
I am having the same issue with streamable http server, sse works fine.
Same issue with basic config over streamable http transport. Will check reverting to SSE
Same issue with my server https://github.com/semgrep/mcp
Any plan to move to the latest version of FastMCP, which has deprecated streamable_http_app and new method http_app works well with Starlette.
I think a found a possible solution using the contextlib,
@contextlib.asynccontextmanager
async def lifespan(app: Starlette):
async with contextlib.AsyncExitStack() as stack:
await stack.enter_async_context(server.session_manager.run())
yield
the server variable is the mcp app instance. I test it withStarlette and Uvicorn and works well with this code:
"""
Run from the repository root:
uv run examples/snippets/servers/streamable_config.py
"""
import contextlib
from starlette.applications import Starlette
from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP
# Stateful server (maintains session state)
server = FastMCP(name="StatefulServer", stateless_http=True)
# Other configuration options:
# Stateless server (no session persistence)
# mcp = FastMCP("StatelessServer", stateless_http=True)
# Stateless server (no session persistence, no sse stream with supported client)
# mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True)
# Add a simple tool to demonstrate the server
@server.tool()
def greet(name: str = "World") -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
# Create a combined lifespan to manage both session managers
@contextlib.asynccontextmanager
async def lifespan(app: Starlette):
async with contextlib.AsyncExitStack() as stack:
await stack.enter_async_context(server.session_manager.run())
yield
# Create the Starlette app with the combined lifespan
app = Starlette(
routes=[
Mount("/", app=server.streamable_http_app())
],
lifespan=lifespan
)
@kevocde Thanks, your solution also works well with FastAPI. My code as below:
from datetime import datetime
from contextlib import asynccontextmanager, AsyncExitStack
import uvicorn
from fastapi import FastAPI, Request
from fastapi.routing import APIRoute
from mcp.server.fastmcp import FastMCP
# region MCP Math
fm_math = FastMCP("Math")
@fm_math.tool()
def add(a: int, b: int) -> int:
"""Adds two integers.
Args:
a: The first integer.
b: The second integer.
"""
return a + b
@fm_math.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two integers.
Args:
a: The first integer.
b: The second integer.
"""
return a * b
# endregion
# region MCP Weather
fm_weather = FastMCP("Weather")
@fm_weather.tool()
def get_weather(location: str) -> str:
return "Cloudy"
@fm_weather.tool()
def get_time() -> str:
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# endregion
# Combine both lifespans
@asynccontextmanager
async def combined_lifespan(app: FastAPI):
# Run both lifespans
async with AsyncExitStack() as stack:
await stack.enter_async_context(fm_math.session_manager.run())
await stack.enter_async_context(fm_weather.session_manager.run())
yield
# endregion
# region Api
api = FastAPI(debug=True, docs_url="/api-docs", lifespan=combined_lifespan)
@api.get("/api/status")
def status():
return {"status": "ok"}
@api.get("/api/list-routes/")
async def list_fastapi_routes(request: Request):
routes_data = []
for route in request.app.routes:
if isinstance(route, APIRoute):
routes_data.append({
"path": route.path,
"name": route.name,
"methods": list(route.methods),
"endpoint": route.endpoint.__name__ # Get the name of the function
})
return {"routes": routes_data}
# endregion
def main():
api.mount("/math", fm_math.streamable_http_app()) # /math/mcp
api.mount("/weather", fm_weather.streamable_http_app()) # /weather/mcp
uvicorn.run(api, host="0.0.0.0", port=8000)
if __name__ == "__main__":
main()
Closing as core issues seems with workaround described above. Please feel free to reopen if you believe there's an issue with the SDK itself that needs to be resolved.