Skip to main content

dishka IoC container integration for FastMCP: scopes, finalization and providers for MCP tools, resources and prompts.

Project description

dishka-fastmcp

CI Coverage PyPI Downloads Python versions License: MIT llms.txt Documentation Context7

dishka IoC container integration for FastMCP. Declare dependencies as FromDishka[Service] in MCP tools, resources and prompts and let dishka resolve them per request — with real scopes, finalization and modular providers, sharing one container with the rest of your application.

from dishka import Provider, Scope, make_async_container, provide
from fastmcp import FastMCP

from dishka_fastmcp import FromDishka, dishka_lifespan, inject, setup_dishka


class Catalog:
    _prices: dict[str, int] = {'book': 12, 'pen': 2}

    def price(self, item: str) -> int:
        return self._prices.get(item, 0)


class AppProvider(Provider):
    catalog = provide(Catalog, scope=Scope.REQUEST)


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


@mcp.tool
@inject
async def get_price(item: str, catalog: FromDishka[Catalog]) -> int:
    return catalog.price(item)


if __name__ == '__main__':
    mcp.run()

The client sees a tool that takes only itemcatalog is injected at call time and never appears in the schema.

Install

uv add dishka-fastmcp        # or: pip install dishka-fastmcp

Requires Python 3.11+, dishka>=1.10.1, fastmcp>=3.2.4,<4.

How it works

Registration time and execution time are separate concerns:

  • @inject sits below the FastMCP decorator. @mcp.tool builds the JSON schema from the function signature. @inject rewrites that signature first, stripping every FromDishka parameter, so the schema the LLM sees contains only the real client-facing arguments. Order matters@inject must be the inner decorator.
  • setup_dishka associates the root container with the FastMCP application. @inject selects that container from the application handling the current operation, then opens and finalizes Scope.REQUEST around the handler. For a sync handler, dependency setup, use, and cleanup all happen in its worker thread.

Scopes

Scope Boundary Lifetime
Scope.APP The whole server Owned by the root container; you close it on shutdown (see below)
Scope.REQUEST One tool call / resource read / prompt render Opened and finalized by @inject around the handler

Scope.SESSION is intentionally not supported. FastMCP does not provide a deterministic teardown boundary for a Dishka session container. Without that boundary, session-scoped resources could not be finalized reliably.

Closing the container

setup_dishka does not own the server lifecycle, so it does not close the root container. dishka_lifespan(container) — used in the example above — closes it (async or sync) and removes its application registration when the server stops, finalizing every Scope.APP provider. If you already have a lifespan, compose it with dishka_lifespan using FastMCP's combine_lifespans. For multiple FastMCP servers hosted by one ASGI application, combine each mcp.http_app().lifespan; see Lifecycle and scopes.

FastMCP may execute regular sync handlers on different worker threads. Consequently, Scope.APP dependencies in a sync container must be thread-safe and their cleanup must not require the thread that created them. Put thread-affine resources such as sqlite3.Connection in Scope.REQUEST, where dishka-fastmcp guarantees creation, use and finalization in one worker thread.

Background tasks

FastMCP's task=True handlers are not supported. The tool call returns as soon as the work is queued, so the request — and with it the REQUEST scope — is already over by the time the worker runs the handler. Injection there fails with DishkaFastMCPError. Keep FromDishka handlers request-bound; if you need background work, resolve dependencies inside the request and pass plain values to the task.

Resources and prompts

@inject works the same on resources and prompts — dependencies are resolved per operation:

@mcp.resource('users://{user_id}')
@inject
async def user(user_id: str, repo: FromDishka[UserRepo]) -> dict:
    return await repo.get(user_id)


@mcp.prompt
@inject
async def summarize(text: str, summarizer: FromDishka[Summarizer]) -> str:
    return await summarizer.run(text)

Sync handlers

FastMCP runs regular sync handlers in a worker thread. Use a sync container (make_container) for them; the REQUEST scope is created and finalized in that same thread:

from dishka import make_container

container = make_container(AppProvider())
setup_dishka(container, mcp)


@mcp.tool
@inject
def compute(x: int, service: FromDishka[Calculator]) -> int:
    return service.square(x)

Async handlers need an async container (make_async_container); mixing the two raises a clear error.

Return values

An ordinary def or async def handler must return its completed value. Returning an awaitable, generator, or async generator is rejected because that work would outlive its REQUEST scope. FastMCP tool handlers defined directly as sync or async generator functions are supported; their scope stays open for the whole iteration.

Accessing FastMCP objects

Add FastMCPProvider to expose the current request's FastMCP objects to your dependencies via dishka's from_context:

from fastmcp import Context

from dishka_fastmcp import FastMCPProvider

container = make_async_container(AppProvider(), FastMCPProvider())


@mcp.tool
@inject
async def notify(message: str, ctx: FromDishka[Context]) -> None:
    await ctx.info(message)

FastMCPProvider also exposes the active FastMCP server.

How this compares

vs fastmcp.dependencies.Depends

FastMCP's own Depends injects a per-call value and hides it from the schema — enough for simple cases. dishka adds what Depends does not have: scopes with finalization, modular providers, and one container shared with the rest of your app (the same graph that feeds your FastAPI or FastStream code can feed your MCP server). Reach for dishka-fastmcp when your MCP server is part of a larger dishka application, or when your dependencies own resources that must be set up and torn down per request.

Relationship to fastmcp-dishka

fastmcp-dishka (Apache-2.0) is an earlier independent implementation of the same core use case. This package uses a different lifecycle model:

  • Scope ownership. @inject opens and closes the request scope where the handler runs, including FastMCP's sync worker thread. Thread-affine REQUEST dependencies are therefore created and finalized on the same thread.
  • Supported boundaries. APP and REQUEST are supported. SESSION is not exposed because FastMCP does not provide a deterministic session teardown boundary. Background-task handlers are rejected because they outlive the originating request.
  • Container lookup. The active container is associated with its owning FastMCP application and resolved through FastMCP's public operation context.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dishka_fastmcp-2.0.0.tar.gz (149.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dishka_fastmcp-2.0.0-py3-none-any.whl (11.6 kB view details)

Uploaded Python 3

File details

Details for the file dishka_fastmcp-2.0.0.tar.gz.

File metadata

  • Download URL: dishka_fastmcp-2.0.0.tar.gz
  • Upload date:
  • Size: 149.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for dishka_fastmcp-2.0.0.tar.gz
Algorithm Hash digest
SHA256 359b733220e6dd1e726921965fbbbd955cda9a43bc31edd2fd23f7a3cb3e54f6
MD5 45320bb61e2e0e0d23e9ab7351afa6b0
BLAKE2b-256 8314df30fb2f39b09999fb69410259498656ba219286598f94d62545fa9b984c

See more details on using hashes here.

Provenance

The following attestation bundles were made for dishka_fastmcp-2.0.0.tar.gz:

Publisher: release.yml on bagowix/dishka-fastmcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dishka_fastmcp-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: dishka_fastmcp-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 11.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for dishka_fastmcp-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fbba109975bc4016ae9ed7fd3bb21aa755bfb1cbe8b461ca1679224ec42a8995
MD5 b84883657774ac169a24680d4e19d4eb
BLAKE2b-256 9d8002b3e3db8d73f5963928dcd49ba9581e411a71946d66825cd54a38df0529

See more details on using hashes here.

Provenance

The following attestation bundles were made for dishka_fastmcp-2.0.0-py3-none-any.whl:

Publisher: release.yml on bagowix/dishka-fastmcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page