#3059·sanic

Does Sanic support mounting sub-applications in ASGI mode?

Author: Pateo-alanduCreated Apr 28, 2025Updated Feb 20, 2026
Labelsfeature request

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe.

I would like to set up an MCP server using Sanic. The MCP server has two main APIs: /sse and /message.

  • /sse: follows the SSE protocol

  • /message: a regular streaming HTTP endpoint

However, there is a unique feature with the /message API: it accepts a request but returns twice

  • first: it returns a response to the current /message request

  • second: it triggers an SSE event to be sent to the /sse connection that shares the same session ID

This is what I am working on.

However, I'm encountering an issue. I want to mount the MCP server as an ASGI sub-application within Sanic's routes. Since Sanic doesn't provide a mount method like FastAPI or Starlette, I need to manually pass the ASGI scope, receive, and send to the MCP server. While the /sse API works as expected, the /message API is throwing an exception.

#!/usr/bin/env python3
# -*- coding:utf-8 -*-


import signal
import uvicorn
from sanic import Sanic
from sanic.request import Request
from sanic.log import logger
from mcp.server.fastmcp import FastMCP
from mcp.server.sse import SseServerTransport


app = Sanic(__name__)

mcp = FastMCP("Sample MCP Server")

sse = SseServerTransport("/messages/")


@mcp.tool()
def hello_world(name: str) -> str:
    """
    Given a person's name, output a greeting

    input:
        name: str -> a person's name
    output:
        str -> a greeting
    """
    return f"hello, {name}!"


@app.route("/messages/", methods=["POST"], stream=True)
async def mcp_messages_handle(request: Request):
    asgi_scope = request.app._asgi_app.transport.scope  # type: ignore
    asgi_receive = request.app._asgi_app.transport._receive  # type: ignore
    asgi_send = request.app._asgi_app.transport._send  # type: ignore

    await sse.handle_post_message(asgi_scope, asgi_receive, asgi_send)


@app.route("/sse", methods=["GET"])
async def mcp_sse_handle(request: Request):
    asgi_scope = request.app._asgi_app.transport.scope  # type: ignore
    asgi_receive = request.app._asgi_app.transport._receive  # type: ignore
    asgi_send = request.app._asgi_app.transport._send  # type: ignore

    async with sse.connect_sse(asgi_scope, asgi_receive, asgi_send) as (
        read_stream,
        write_stream,
    ):
        # Run the MCP server with the established streams
        await mcp._mcp_server.run(
            read_stream,
            write_stream,
            mcp._mcp_server.create_initialization_options()
        )


def stopServer(signal, frame):
    logger.info("Stopping Sanic Server...")
    app.stop()
    logger.info("Stoped Sanic Server!")


if __name__ == "__main__":
    logger.info("Starting Sanic Server...")

    signal.signal(signal.SIGINT, lambda signal, frame: stopServer(signal, frame))

    signal.signal(signal.SIGTERM, lambda signal, frame: stopServer(signal, frame))

    app.config.host = "0.0.0.0"
    app.config.port = "8080"

    logger.debug(app.config)

    uvicorn.run("main:app", port=8080, log_level="info")

The above is the method to run a sub-application using Sanic, but the /messages API encounters an exception.

INFO:     127.0.0.1:52400 - "POST /messages/?session_id=1c24c44a046d4f3486126ed3af87eeb3 HTTP/1.1" 202 Accepted
{'type': 'http.response.body', 'body': b'Accepted'}  # It can be observed that the first request is returned successfully.
Main  2025-04-28 11:12:48 +0800 ERROR:    Exception occurred while handling uri: 'http://127.0.0.1:8080/messages/?session_id=1c24c44a046d4f3486126ed3af87eeb3'
Traceback (most recent call last):
  File "handle_request", line 147, in handle_request
    signal_router (Optional[SignalRouter]): The signal router to
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<2 lines>...
        use for the application. Defaults to `None`.
        ^^^^^
sanic.exceptions.ServerError: Invalid response type None (need HTTPResponse)
                    ERROR Exception occurred while handling uri: 'http://127.0.0.1:8080/messages/?session_id=1c24c44a046d4f3486126ed3af87eeb3'

# However, an exception occurs when the second response is returned.
{'type': 'http.response.start', 'status': 500, 'headers': <generator object BaseHTTPResponse.processed_headers.<locals>.<genexpr> at 0x7f3f9b4a7880>}

Main  2025-04-28 11:12:48 +0800 ERROR:    Exception occurred while handling uri: 'http://127.0.0.1:8080/messages/?session_id=1c24c44a046d4f3486126ed3af87eeb3'
Traceback (most recent call last):
  File "handle_request", line 147, in handle_request
    signal_router (Optional[SignalRouter]): The signal router to
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<2 lines>...
        use for the application. Defaults to `None`.
        ^^^^^
sanic.exceptions.ServerError: Invalid response type None (need HTTPResponse)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/sanic/asgi.py", line 258, in __call__
    await self.sanic_app.handle_request(self.request)
  File "handle_request", line 156, in handle_request
        Defaults to `False`.
        ^^^^^^^^^^^^^^^^^^^^
    log_config (Optional[Dict[str, Any]]): The logging configuration
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        to use for the application. Defaults to `None`.

  File "handle_exception", line 125, in handle_exception
    restrictions as a Python module name, however, it can contain
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/sanic/response/types.py", line 119, in send
    await self.stream.send(
    ...<2 lines>...
    )
  File "/usr/local/lib/python3.13/site-packages/sanic/asgi.py", line 231, in send
    await self.transport.send(
    ...<5 lines>...
    )
  File "/usr/local/lib/python3.13/site-packages/sanic/models/asgi.py", line 97, in send
    await self._send(data)
  File "/usr/local/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line 555, in send
    raise RuntimeError(msg % message_type)
RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.

If Sanic provided a route mount method like FastAPI, it would be much easier to implement this.

app = Starlette(
    debug=self.settings.debug,
    routes=[
        Route("/sse", endpoint=mcp_sse_handle),
        Mount("/messages/", app=sse.handle_post_message),
    ],
)

uvicorn.run("main:app", port=8080, log_level="info")

Describe the solution you'd like

The app._asgi_app.create() method initializes Sanic as an ASGI application. Based on this method, is it possible to add a mount method to initialize a sub-ASGI application?

Additional context

There is an implementation of the mount method in Starlette.