# Dependencies

A tool's arguments come from the model. Some values never should: a price looked up from your records, a confirmation only a person can give, anything the model could get wrong by inventing it.

**Dependencies** are parameters filled by your own functions. You annotate the parameter, name the function, and the SDK calls it before your tool runs.

## Declare one

Wrap the parameter's type in `Annotated[...]` and add `Resolve(fn)`:

```python title="server.py" hl_lines="18-19 23"
# docs_src/dependencies/tutorial001.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


@mcp.tool()
async def reserve_book(title: str, stock: Annotated[Stock, Resolve(check_stock)]) -> str:
    """Reserve a copy of a book."""
    if stock.copies == 0:
        return f"{title!r} is out of stock."
    return f"Reserved {title!r} ({stock.copies - 1} copies left)."
```

* `check_stock` is a **resolver**: a plain function the SDK runs before `reserve_book`, whose return value becomes the `stock` argument.
* Its `title` parameter is the tool's own `title` argument, matched **by name**. The resolver sees exactly the validated value the tool body will see.
* The tool body starts from a `Stock` that already exists. No lookup code in the tool, no "what if it's missing" preamble.

!!! info
    If you've used FastAPI, this is `Depends`. Same move, same reason: the function declares what
    it needs, the framework supplies it, and the wiring lives in the type annotation.

### Invisible to the model

Here is the input schema `tools/list` reports for `reserve_book`:

```json
{
  "type": "object",
  "properties": {
    "title": {"title": "Title", "type": "string"}
  },
  "required": ["title"],
  "title": "reserve_bookArguments"
}
```

One property. Like the `Context` in **[The Context](https://py.sdk.modelcontextprotocol.io/handlers/context/index.md)**, a resolved parameter is a contract between you and the SDK: `stock` is not in the schema, the model is never told about it, and a client that sends a `stock` value anyway is ignored. The resolver's value is the only one your tool can receive.

That last part is the point. A parameter the model cannot supply is a parameter the model cannot get wrong.

### Try it

Run the server with the MCP Inspector:

```console
uv run mcp dev server.py
```

The form for `reserve_book` has a single `title` field. `stock` is nowhere on it. Call it with `Dune`:

```text
Reserved 'Dune' (6 copies left).
```

The tool body never looked anything up: `check_stock` ran first, and the `Stock` it returned arrived as an argument. Try `Neuromancer` and the same resolver hands the tool a zero.

!!! tip
    You could just call `check_stock(title)` in the tool body. Declare it as a dependency when the
    value deserves more than a helper call: every tool that needs stock declares the same parameter,
    and the SDK runs the resolver at most once per call, no matter how many declare it. The next
    sections add the rest: resolvers that depend on each other, and resolvers that ask the user.

## Dependencies of dependencies

A resolver can declare its own dependencies, with the same annotation:

```python title="server.py" hl_lines="22 29-30"
# docs_src/dependencies/tutorial002.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


async def estimate_delivery(stock: Annotated[Stock, Resolve(check_stock)]) -> str:
    return "tomorrow" if stock.copies > 0 else "in 2-3 weeks"


@mcp.tool()
async def order_book(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
    delivery: Annotated[str, Resolve(estimate_delivery)],
) -> str:
    """Order a book from the shop."""
    if stock.copies == 0:
        return f"{title!r} is on backorder; it would arrive {delivery}."
    return f"Ordered {title!r}; it arrives {delivery}."
```

* `estimate_delivery` depends on `check_stock`. The SDK runs the graph in order: stock first, then the estimate, then the tool.
* Both `stock` and `delivery` ultimately need `check_stock`, but it runs **once per call**. One inventory lookup, two consumers.
* There is nothing to register. The graph *is* the annotations.

!!! check
    Don't take once-per-call on faith. Put a `print` in `check_stock` and call `order_book` from the
    Inspector: one line per call. Two consumers, one lookup.

The SDK analyses the graph when the tool is registered, not when it is called. A parameter it can't classify - not a `Context`, not a `Resolve(...)`, not a tool argument's name - and a cycle of resolvers both raise `InvalidSignature` at startup. Your server fails before a client ever connects, with the offending parameter or resolver named in the error.

A resolver's parameters resolve exactly like a tool's: another `Resolve(...)`, the tool's own arguments by name, or the `Context` - `ctx.headers`, the lifespan object, all of it.

!!! warning
    On HTTP transports the `Context` includes `ctx.headers`. Headers are **client-supplied input**,
    like any tool argument: fine for a locale or a feature flag, never an identity. Who the caller
    is comes from your authorization layer (**[Authorization](https://py.sdk.modelcontextprotocol.io/run/authorization/index.md)**), not from a header anyone can set.

!!! tip
    *Once per call* means exactly that: the next `tools/call` runs `check_stock` again. A resource
    that should outlive a request - a database pool, an HTTP client - belongs in **[Lifespan](https://py.sdk.modelcontextprotocol.io/handlers/lifespan/index.md)**, and
    a resolver can reach it through `ctx.request_context.lifespan_context`.

## Ask when you must

A resolver doesn't have to know the answer. It can return `Elicit(message, Model)` and the SDK asks the user - the **[Elicitation](https://py.sdk.modelcontextprotocol.io/handlers/elicitation/index.md)** machinery, run for you:

```python title="server.py" hl_lines="26-32 39"
# docs_src/dependencies/tutorial003.py
from typing import Annotated

from pydantic import BaseModel, Field

from mcp.server import MCPServer
from mcp.server.mcpserver import Elicit, Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


class Backorder(BaseModel):
    confirm: bool = Field(description="Order anyway and wait?")


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


async def confirm_backorder(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
) -> Backorder | Elicit[Backorder]:
    if stock.copies > 0:
        return Backorder(confirm=True)  # in stock: nothing to ask
    return Elicit(f"{title!r} is out of stock (2-3 weeks). Order anyway?", Backorder)


@mcp.tool()
async def order_book(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
    backorder: Annotated[Backorder, Resolve(confirm_backorder)],
) -> str:
    """Order a book from the shop."""
    if not backorder.confirm:
        return "No order placed."
    if stock.copies == 0:
        return f"Backordered {title!r}; it ships in 2-3 weeks."
    return f"Ordered {title!r}."
```

* In stock: `confirm_backorder` returns a `Backorder` directly. **No question, no round-trip.** The user is only interrupted when their answer matters.
* Out of stock: the SDK sends the elicitation, validates the answer against `Backorder`, and injects it. Your resolver never touches the protocol.
* The tool reads `backorder.confirm` like any other argument. Answering **no** is still an answer: the elicitation is accepted with `confirm=False`, the tool runs, and no order is placed. Asking became a precondition, not plumbing in the tool body.

And if the user won't answer at all - declines the question, or cancels it?

!!! check
    Run `order_book` for `Neuromancer` and decline the question. With the annotation written as
    `Annotated[Backorder, Resolve(...)]` the tool body never runs; the call fails with an error
    result the model can read:

    ```text
    Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline
    ```

That's the right default for a precondition: no answer, no order. When declining is an outcome your tool wants to handle - skip the backorder but still suggest another title - annotate `ElicitationResult[Backorder]` instead and the tool receives the full accept/decline/cancel outcome to branch on. **[Elicitation](https://py.sdk.modelcontextprotocol.io/handlers/elicitation/index.md)** shows that form, and everything else about asking: the schema rules, the three answers, the client's side of the conversation.

!!! info
    The framework picks the question's transport from the negotiated protocol version; the code
    above is identical on both. On **2026-07-28** and later the question rides inside a
    multi-round-trip `tools/call` - the server returns it, the client's `elicitation_callback`
    answers it, and the `Client` retries the call for you (**[Multi-round-trip requests](https://py.sdk.modelcontextprotocol.io/handlers/multi-round-trip/index.md)**). On
    **2025-11-25** and earlier it is a synchronous elicitation request mid-call. Each question is
    asked exactly once per call - a guarantee about the question, not the resolver. In the
    multi-round-trip form any resolver may run again whenever the call resumes after a question,
    so code before a `return Elicit(...)` runs on each of those rounds; the recorded answer then
    satisfies the repeated question without prompting the user again. A recorded answer is only
    ever consulted when the resolver asks; a resolver that answers *without* asking, like
    `check_stock`, always supplies its own computed value. Because each answer is matched back to
    its question, an eliciting resolver must derive its question deterministically from the
    tool's arguments and earlier answers. A per-call generated value (a `default_factory` id, a
    timestamp) is re-derived on each round and must not appear in a question the answer is meant
    to bind to. A question built from such volatile data makes every recorded answer look stale,
    so the server re-asks it on every round until the client's round limit ends the call.

## Ask the client, not the user

Elicitation is one of the three questions a resolver can ask, and the multi-round-trip flow allows no others. The other two go to the **client** rather than the user: return `Sample(...)` to run an LLM call through the client (a `sampling/createMessage` request), or `ListRoots()` to fetch the client's current roots. Neither has an accept/decline outcome; the consumer annotates the result type directly, `CreateMessageResult` (`CreateMessageResultWithTools` when the request carries `tools` or `tool_choice`) or `ListRootsResult`:

```python title="server.py" hl_lines="10-15 21"
# docs_src/dependencies/tutorial004.py
from typing import Annotated

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve, Sample
from mcp.types import CreateMessageResult, SamplingMessage, TextContent

mcp = MCPServer("Bookshop")


def suggest_title(genre: str) -> Sample:
    prompt = f"Suggest one {genre} book title. Answer with the title only."
    return Sample(
        [SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
        max_tokens=50,
    )


@mcp.tool()
async def recommend_book(
    genre: str,
    suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)],
) -> str:
    """Recommend a book in the given genre."""
    title = suggestion.content.text if suggestion.content.type == "text" else "the classics"
    return f"Today's {genre} pick: {title}"
```

* The framework routes these exactly like `Elicit`: inside the multi-round-trip `tools/call` on **2026-07-28**, over the standalone server->client request on **2025-11-25**. An undeclared capability refuses the call with a `-32021` protocol error (`sampling`, `roots`, form-mode `elicitation`; `sampling.tools` when the request carries `tools` or `tool_choice`).
* Everything the info box above says about questions applies unchanged: a `Sample` request is matched to its recorded result by its exact rendering, so build it deterministically from the tool's arguments and earlier answers; the client then pays for the LLM call once per tool call, not once per round. The recorded result rides `request_state` for the rest of the call, so a very large completion makes every remaining round-trip heavier.
* The standalone sampling and roots *features* are deprecated at 2026-07-28 (SEP-2577). New servers that need the client's model ask through this carrier; servers that don't should integrate with an LLM provider directly. `include_context` values other than `"none"` are themselves deprecated; avoid them.

## Recap

* `Annotated[T, Resolve(fn)]` on a tool parameter: the SDK runs `fn` and injects its return value.
* A resolved parameter is invisible to the model and cannot be supplied by a client. Values the model must not invent - prices, identities, permissions - belong here.
* A resolver's parameters are resolved the same way: the `Context`, another `Resolve(...)`, or a tool argument by name. The graph runs each resolver at most once per round, however many consumers it has; each question is asked exactly once, and any resolver may run again when a call resumes after a question.
* Bad graphs fail at registration with `InvalidSignature`, not mid-call.
* Return `Elicit(message, Model)` to ask the user, only when you have to. Unwrapped annotations abort on decline; `ElicitationResult[T]` lets the tool branch.
* Return `Sample(...)` or `ListRoots()` to ask the client for an LLM completion or the roots list; the plain result is injected.

The state your server builds once at startup, and how a handler reaches it, is the **[Lifespan](https://py.sdk.modelcontextprotocol.io/handlers/lifespan/index.md)** page.
