dishka IoC container integration for FastMCP: scopes, finalization and providers for MCP tools, resources and prompts.
Project description
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>=3.2.4,<4.
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.setup_dishkaregisters a middleware that publishes the root container and current FastMCP objects throughContextVars.@injectthen opens and finalizesScope.REQUESTaround the handler in the thread where it executes. This keeps sync dependency setup, use and cleanup in the same worker thread.
Scopes
| Scope | Boundary | Lifetime |
|---|---|---|
Scope.APP |
The whole server | Created by make_async_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's middleware exposes
a session-start hook (on_initialize) but no session-teardown hook, so a
session-lifetime container could never be finalized deterministically. Rather
than ship a SESSION scope that silently behaves like REQUEST, dishka-fastmcp
offers only the two scopes it can honor. If FastMCP adds a session-teardown
hook, SESSION support will follow.
Closing the container
setup_dishka registers the middleware but 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) when the server stops, finalizing every
Scope.APP provider. If you already have your own lifespan, close the container
in its shutdown path instead (await container.close(), or container.close()
for a sync container).
FastMCP may execute 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 tools
FastMCP runs sync tools in a worker thread (run_in_thread=True by default).
Use a sync container (make_container) for sync handlers; the request
container propagates into the worker thread correctly:
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.
Accessing FastMCP objects
Add FastMCPProvider to expose the current request's FastMCP objects to your
dependencies via dishka's from_context:
from fastmcp.server.context 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 FastMCP server and the raw request params
(CallToolRequestParams, ReadResourceRequestParams, GetPromptRequestParams)
of the operation in flight.
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.
vs fastmcp-dishka
fastmcp-dishka (Apache-2.0) is prior art covering the same surface, and both
packages build on dishka's public wrap_injection helper. Differences:
- Scopes we can honor. dishka-fastmcp offers
APPandREQUESTonly, for the reason above.fastmcp-dishkaalso exposesSESSION, which on current FastMCP resolves per request rather than per session. - No coupling to FastMCP internals. The request container travels through a
ContextVarthis package owns, not a private FastMCP attribute. - Quality bar. 100% test coverage,
mypyandpyrightin strict mode, and a CI matrix across Python 3.11–3.14.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dishka_fastmcp-1.0.0.tar.gz.
File metadata
- Download URL: dishka_fastmcp-1.0.0.tar.gz
- Upload date:
- Size: 132.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71417b75affa0228cb47d04a3cf806c0b2f8e51e073ffa60303c663dd19a611f
|
|
| MD5 |
58e1b1179906304cd281c6df8c2df947
|
|
| BLAKE2b-256 |
8e36d92728a6b9033553546f23047cbe37d3216c98fd69c59e5f1e448dd2e799
|
Provenance
The following attestation bundles were made for dishka_fastmcp-1.0.0.tar.gz:
Publisher:
release.yml on bagowix/dishka-fastmcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dishka_fastmcp-1.0.0.tar.gz -
Subject digest:
71417b75affa0228cb47d04a3cf806c0b2f8e51e073ffa60303c663dd19a611f - Sigstore transparency entry: 2214160613
- Sigstore integration time:
-
Permalink:
bagowix/dishka-fastmcp@d4e7bd0689998591f58c00f9e7bc015c2aabbd1f -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/bagowix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d4e7bd0689998591f58c00f9e7bc015c2aabbd1f -
Trigger Event:
push
-
Statement type:
File details
Details for the file dishka_fastmcp-1.0.0-py3-none-any.whl.
File metadata
- Download URL: dishka_fastmcp-1.0.0-py3-none-any.whl
- Upload date:
- Size: 11.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0cf8db2056bc699fad7bfdd676f6e91acb22f9fb7519790c95b611932f4830a1
|
|
| MD5 |
9bb308eca684667323cfb1bd5b930fd6
|
|
| BLAKE2b-256 |
9f503532a370223d9da8a77c8b4aa19a943d5b383ab905fa535f291d1207a25d
|
Provenance
The following attestation bundles were made for dishka_fastmcp-1.0.0-py3-none-any.whl:
Publisher:
release.yml on bagowix/dishka-fastmcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dishka_fastmcp-1.0.0-py3-none-any.whl -
Subject digest:
0cf8db2056bc699fad7bfdd676f6e91acb22f9fb7519790c95b611932f4830a1 - Sigstore transparency entry: 2214160837
- Sigstore integration time:
-
Permalink:
bagowix/dishka-fastmcp@d4e7bd0689998591f58c00f9e7bc015c2aabbd1f -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/bagowix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d4e7bd0689998591f58c00f9e7bc015c2aabbd1f -
Trigger Event:
push
-
Statement type: