# dishka-fastmcp > dishka IoC container integration for FastMCP. Declare dependencies as > `FromDishka[Service]` in MCP tools, resources and prompts; dishka resolves them > per request with real scopes, finalization and modular providers, sharing one > container with the rest of the application. Install: `uv add dishka-fastmcp` (or `pip install dishka-fastmcp`). Requires Python 3.11+, `dishka>=1.10.1`, `fastmcp>=4.0.0,<5`. ## Rules that matter - **Put `@inject` BELOW the FastMCP decorator** (`@mcp.tool` / `@mcp.resource` / `@mcp.prompt`). `@inject` rewrites the signature to strip `FromDishka` parameters; FastMCP then builds the JSON schema from the cleaned signature, so injected services never appear as tool arguments. Wrong order leaks the service into the schema. - **Call `setup_dishka(container, mcp)` once** before running the server. - **Routers need no setup of their own.** Routers mounted into a server whose lifespan includes `dishka_lifespan(container)` use that container during the MCP requests the server serves (any depth, with or without `namespace`). A router mounted into several servers uses the serving server's container. A router with its own `setup_dishka` keeps its container, and so does one with its own `dishka_lifespan` if it is mounted before the server starts. Routers below it still take the serving server's, silently, so give them `setup_dishka` when they need the middle one. A direct `server.call_tool()` outside an MCP request does not reach mounted routers; test them through `Client(server)`. - **Async handlers need `make_async_container`; sync handlers need `make_container`** (regular sync handlers run in a FastMCP worker thread). Mixing the two raises `DishkaFastMCPError`. - **Scopes: `Scope.APP`** (server lifetime) and **`Scope.REQUEST`** (one tool call / resource read / prompt render, finalized when it ends). `Scope.SESSION` is intentionally unsupported: FastMCP has no session-teardown hook, so a session-lifetime scope cannot be finalized deterministically. - **Close the root container on shutdown** to finalize `Scope.APP` providers: pass `dishka_lifespan(container)` to `FastMCP(lifespan=...)`. Compose this helper with an existing lifespan so shutdown also removes the container's application registration. If the container is shared with FastAPI, FastStream, a worker or a test session, list `mcp.http_app().lifespan` first in `combine_lifespans`, or pass `dishka_lifespan(container, finalize_container=False)` and close it where it is owned. Dishka re-creates APP dependencies on `get()` after `close()` without an error, so with a wrong order shutdown code silently opens new resources, which leak unless the container is closed again. - **Sync `Scope.APP` dependencies must be thread-safe.** FastMCP may use different worker threads, and APP cleanup runs from the server lifespan. Put thread-affine resources in `Scope.REQUEST`, which is created and finalized in one worker. - **`task=True` handlers are not supported.** They run in a `fastmcp-tasks` worker, outside the request that queued them, so injection there raises `DishkaFastMCPError`. Resolve dependencies inside the request and pass plain values to the task. - **Return completed values from ordinary handlers.** Returning an awaitable, generator, or async generator is rejected because it would run after the handler returns, when the REQUEST scope may already be finalized. Tool handlers defined directly as generator functions remain supported for the whole iteration. - **For a scope over the whole MCP request, add `DishkaMiddleware()` first** in `FastMCP(middleware=[...])`. Dynamic component providers, `FunctionTool` callables and middleware then resolve REQUEST dependencies with `await get_request_container().get(T)`, and async `@inject` handlers share the same instances, finalized once before the response goes out. FastMCP also lists providers outside requests (on startup), where `get_request_container()` raises; return no components there. It raises in completion and extension-method handlers as well. Sync handlers keep their own worker-thread scope, and a router set up with a different container keeps its own scopes and gets no request container. ## API - `setup_dishka(container, app)` — associate a root container with a `FastMCP` app. - `inject` — decorator binding `FromDishka[...]` parameters; auto-detects sync vs async. - `FromDishka[T]` — mark a parameter for injection (re-exported from dishka). - `FastMCPProvider()` — add to the container to inject the current request's `fastmcp.Context` or `fastmcp.FastMCP` via dishka's `from_context`. Operation arguments remain regular tool, resource, or prompt parameters. - `dishka_lifespan(container, *, finalize_container=True)` — build a FastMCP `Lifespan` that registers the container, hands it to mounted routers and closes it (async or sync) on shutdown; `finalize_container=False` leaves closing to the container's owner. Compose it with `combine_lifespans`, or with `|` when the other lifespan is a FastMCP `@lifespan` function. - `DishkaMiddleware()` — FastMCP middleware that opens one REQUEST scope per MCP request (list requests included) and shares it with async `@inject` handlers. Put it first; it needs an `AsyncContainer`. - `get_request_container()` — the REQUEST `AsyncContainer` of the current MCP request, shared with async `@inject` handlers; raises `DishkaFastMCPError` outside the middleware's scope. - `DishkaFastMCPError` — raised on integration misuse: a missing or wrong-kind container, a different container already registered for the app, a returned awaitable or generator, injection inside a `task=True` worker, `get_request_container()` outside a request scope, or `DishkaMiddleware` with a sync container. ## Usage ```python 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 = {'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) # client sees only `item` in the schema ``` ## Links - [Documentation](https://bagowix.github.io/dishka-fastmcp/) - [Context7](https://context7.com/bagowix/dishka-fastmcp) - [Agent Skill](https://github.com/bagowix/dishka-fastmcp/blob/main/skills/dishka-fastmcp/SKILL.md): a workflow for wiring dishka into a FastMCP server with a coding agent; install with `npx skills add bagowix/dishka-fastmcp` - [README](https://github.com/bagowix/dishka-fastmcp/blob/main/README.md) - [Source](https://github.com/bagowix/dishka-fastmcp) - [PyPI](https://pypi.org/project/dishka-fastmcp/) - [dishka documentation](https://dishka.readthedocs.io/) - [FastMCP documentation](https://gofastmcp.com/)