Skip to content

Lifecycle and scopes

dishka-fastmcp separates FastMCP registration from operation execution. The decorator cleans the public signature during registration. At execution time, @inject resolves the active FastMCP application and owns the request scope.

Scope boundaries

Scope Boundary Owner
Scope.APP Server lifetime Root container, closed by the FastMCP lifespan
Scope.REQUEST One tool call, resource read, or prompt render @inject

Use dishka_lifespan when the FastMCP server owns the container:

container = make_async_container(AppProvider())
mcp = FastMCP('app', lifespan=dishka_lifespan(container))

If the FastMCP server already has a lifespan, combine it with dishka_lifespan:

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastmcp import FastMCP
from fastmcp.utilities.lifespan import combine_lifespans


@asynccontextmanager
async def application_lifespan(
    app: FastMCP,
) -> AsyncIterator[dict[str, object]]:
    resource = await create_resource()
    try:
        yield {'resource': resource}
    finally:
        await resource.close()


mcp = FastMCP(
    'app',
    lifespan=combine_lifespans(
        application_lifespan,
        dishka_lifespan(container),
    ),
)
setup_dishka(container, mcp)

combine_lifespans enters lifespans in argument order and exits them in reverse order. Placing dishka_lifespan last closes the Dishka container before the other lifespan releases its resources.

The combined lifespan above belongs to a FastMCP instance. When composing FastMCP with FastAPI or Starlette, keep dishka_lifespan on the FastMCP server and combine the lifespan exposed by mcp.http_app() at the ASGI layer. This is also the correct pattern for hosting multiple independent FastMCP servers:

from fastapi import FastAPI


first = FastMCP('first', lifespan=dishka_lifespan(first_container))
second = FastMCP('second', lifespan=dishka_lifespan(second_container))
setup_dishka(first_container, first)
setup_dishka(second_container, second)

first_app = first.http_app()
second_app = second.http_app()
app = FastAPI(
    lifespan=combine_lifespans(
        first_app.lifespan,
        second_app.lifespan,
    ),
)
app.mount('/first', first_app)
app.mount('/second', second_app)

Do not combine dishka_lifespan(first_container) and dishka_lifespan(second_container) directly at the ASGI layer: combine_lifespans passes the same ASGI application to every lifespan. Each mcp.http_app().lifespan adapter preserves the corresponding FastMCP instance.

Sync request finalization

FastMCP offloads regular sync handlers to worker threads. @inject opens and closes the Dishka request container inside the wrapped handler, so thread-affine resources stay in one thread:

import sqlite3
from collections.abc import Iterator

from dishka import Provider, Scope, provide


class DatabaseProvider(Provider):
    @provide(scope=Scope.REQUEST)
    def connection(self) -> Iterator[sqlite3.Connection]:
        connection = sqlite3.connect('app.db')
        try:
            yield connection
        finally:
            connection.close()

Sync Scope.APP dependencies must still be thread-safe because calls may run on different workers and APP cleanup happens during server shutdown.

Limitations

Scope.SESSION is not supported because FastMCP does not provide a deterministic teardown boundary for a Dishka session container.

Handlers registered with task=True are also unsupported. Background execution starts after the originating request context has ended. Resolve dependencies during the request and pass plain values to background work.