dishka-fastmcp
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 item — catalog 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>=4.0.0,<5 and mcp>=2.0.0,<3.
Servers still on FastMCP 3.x should stay on dishka-fastmcp 2.0.x.
With a coding agent
The repository ships an Agent Skill that walks a coding agent through adding dishka-fastmcp to a FastMCP server: the container and lifespan, decorator order, sync and async handlers, mounted routers, the request-wide scope, and tests that assert the tool schema. It installs into Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI and the other agents that read the open skills format:
npx skills add bagowix/dishka-fastmcp
Agents that read documentation directly can use llms.txt or Context7.
How it works
Registration time and execution time are separate concerns:
@injectsits below the FastMCP decorator.@mcp.toolbuilds the JSON schema from the function signature.@injectrewrites that signature first, stripping everyFromDishkaparameter, so the schema the LLM sees contains only the real client-facing arguments. Order matters —@injectmust be the inner decorator. Placed above, it fails at import: FastMCP has registered the bare function by then, and the client would fill in the dependency itself.setup_dishkaassociates the root container with the FastMCP application.@injectselects that container from the application handling the current operation, then opens and finalizesScope.REQUESTaround the handler, or shares the scopeDishkaMiddlewareopened for the whole request. For a sync handler, dependency setup, use, and cleanup all happen in its worker thread.- Mounted routers use the serving server's container.
dishka_lifespanputs the container into the lifespan state of the server you serve, and routers mounted into it at any depth read it from there during MCP requests. A router mounted into several servers uses the container of the one serving the request. A router with its ownsetup_dishkakeeps its own container, and so does one with its owndishka_lifespanif it is mounted before the server starts. Routers below such a router still take the serving root's. Directserver.call_tool()calls outside an MCP request and servers behindcreate_proxyare not covered; see Mounted servers.
Scopes
| Scope | Boundary | Lifetime |
|---|---|---|
Scope.APP |
The whole server | Owned by the root container; dishka_lifespan closes it on shutdown, or its owner does (see below) |
Scope.SESSION |
One MCP connection | Opt-in: opened by DishkaMiddleware(session_scope=True), finalized when the connection closes |
Scope.REQUEST |
One tool call / resource read / prompt render | Opened and finalized by @inject around the handler, or by DishkaMiddleware around the whole MCP request |
Scope.SESSION follows the MCP connection, so it spans several requests only
where a connection does: 2025 handshake clients over stdio, stateful Streamable
HTTP, SSE or an in-memory Client(server, mode='legacy'). The 2026-07-28 protocol, which FastMCP 4's own
Client speaks by default, and stateless HTTP build a fresh connection for every
request, and there a SESSION dependency lives for one request. See
Session scope.
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, or through |
when it is a FastMCP @lifespan function. For multiple
FastMCP servers hosted by one ASGI application, combine each
mcp.http_app().lifespan; see
Lifecycle and scopes.
If the same container also serves FastAPI, FastStream, a worker or a test
session, the MCP server must not close it while they still use it. List
mcp.http_app().lifespan first in combine_lifespans, or pass
dishka_lifespan(container, finalize_container=False) and close the container
where it is owned; see
Sharing the container.
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. In FastMCP 4 they come from
the optional fastmcp-tasks extension and run in a task worker, detached from the
request that queued them and possibly in another process. @inject detects the
worker and raises DishkaFastMCPError there. 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())
mcp = FastMCP('sync', lifespan=dishka_lifespan(container))
setup_dishka(container, mcp)
@mcp.tool
@inject
def get_price_sync(item: str, catalog: FromDishka[Catalog]) -> int:
return catalog.price(item)
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: FastMCP would
run that work after the handler returns, when its REQUEST scope may already be
finalized. 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 request_id(ctx: FromDishka[Context]) -> str:
return ctx.request_id
FastMCPProvider also exposes the active FastMCP server.
Request scope for the whole request
@inject scopes one handler call. To give dynamic component providers,
FunctionTool callables and middleware the same REQUEST dependencies, add
DishkaMiddleware first in the middleware list and resolve through
get_request_container():
from dishka_fastmcp import DishkaMiddleware, get_request_container
mcp = FastMCP(
'shop',
lifespan=dishka_lifespan(container),
middleware=[DishkaMiddleware()],
)
async def current_user() -> User:
return await get_request_container().get(User)
Every request then gets one scope, list requests included, shared by async
@inject handlers and finalized once before the response goes out. It needs an
AsyncContainer. Without the middleware nothing changes; see
Request scope for the whole MCP request
for the rules.
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.
fastmcp-dishka
fastmcp-dishka is an earlier,
independent package with a similar name and the same goal. The two are not
interchangeable: install one, and import dishka_fastmcp for this one. When
moving over, add dishka_lifespan(container) to the server and check its
Scope.SESSION providers: here a SESSION dependency is rebuilt for every request
unless you add
DishkaMiddleware(session_scope=True),
and even then it spans requests only on 2025 handshake connections.
License
MIT — see LICENSE.
Metadata
Release files for dishka-fastmcp 3.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| dishka_fastmcp-3.3.1.tar.gz | 204.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dishka_fastmcp-3.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 223.1 kB
Release files / dishka_fastmcp-3.3.1.tar.gz
| Download URL | dishka_fastmcp-3.3.1.tar.gz |
|---|---|
| Size | 204.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4a1158800e85d7a5542d55792a101cad11c599ca635132d0e90e89fc64a691b9
|
|
BLAKE2b-256 checksum How to use checksums |
170d31e59101db4682b0683f194f60f5728f89b2ff5ccfcf43dd679b4d7685f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / dishka_fastmcp-3.3.1-py3-none-any.whl
| Download URL | dishka_fastmcp-3.3.1-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cab762de50dbc1533e46f3092a5f7c82db5fcf81f68d94b1dd6ff9e2349eae1f
|
|
BLAKE2b-256 checksum How to use checksums |
893ff0331f3d0c8de0af02398029f6ebac47fa5f58f0bd1a80b1b649901ba45d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for dishka-fastmcp 3.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for dishka_fastmcp-3.3.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.