python-sdk icon indicating copy to clipboard operation
python-sdk copied to clipboard

how to manage multi streamable http server lifespan

Open AniviaTn opened this issue 7 months ago • 7 comments

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().

AniviaTn avatar May 14 '25 08:05 AniviaTn

I have the same problem

Shaw-shaw avatar May 15 '25 02:05 Shaw-shaw

thank you for reporting this! Updated the example in readme

ihrpr avatar May 15 '25 08:05 ihrpr

its failing for me too for basic example with mcp streamable HTTP transport, what is the solution?

AydarAkhmetzyanov avatar May 18 '25 00:05 AydarAkhmetzyanov

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

kanweiwei avatar May 22 '25 00:05 kanweiwei

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)

`

bygzyz avatar May 23 '25 06:05 bygzyz

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.

Nayjest avatar May 29 '25 16:05 Nayjest

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?

ihrpr avatar May 29 '25 16:05 ihrpr

@bygzyz Please see: https://www.reddit.com/r/mcp/comments/1kizgw2/fastapi_fastmcp_integration_question/

thanusiak avatar Jun 02 '25 11:06 thanusiak

I have the same problem! starlette mount multi mcp server. sse can work, but streamable http not.

ZhangHaoWeb avatar Jun 06 '25 10:06 ZhangHaoWeb

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

ZhangHaoWeb avatar Jun 06 '25 10:06 ZhangHaoWeb

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.

caravin avatar Jun 06 '25 22:06 caravin

I am having the same issue with streamable http server, sse works fine.

fabriciojoc avatar Jun 10 '25 17:06 fabriciojoc

Same issue with basic config over streamable http transport. Will check reverting to SSE

Nx5 avatar Jun 11 '25 14:06 Nx5

Same issue with my server https://github.com/semgrep/mcp

DrewDennison avatar Jun 12 '25 19:06 DrewDennison

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.

ranpariyachetan avatar Jun 29 '25 08:06 ranpariyachetan

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 avatar Aug 26 '25 14:08 kevocde

@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()

gsw945 avatar Sep 25 '25 11:09 gsw945

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.

felixweinberger avatar Oct 03 '25 14:10 felixweinberger