|
| 1 | +# Dependencies |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +**Dependencies** are parameters filled by your own functions. You annotate the parameter, name the function, and the SDK calls it before your tool runs. |
| 6 | + |
| 7 | +## Declare one |
| 8 | + |
| 9 | +Wrap the parameter's type in `Annotated[...]` and add `Resolve(fn)`: |
| 10 | + |
| 11 | +```python title="server.py" hl_lines="18-19 23" |
| 12 | +--8<-- "docs_src/dependencies/tutorial001.py" |
| 13 | +``` |
| 14 | + |
| 15 | +* `check_stock` is a **resolver**: a plain function the SDK runs before `reserve_book`, whose return value becomes the `stock` argument. |
| 16 | +* 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. |
| 17 | +* The tool body starts from a `Stock` that already exists. No lookup code in the tool, no "what if it's missing" preamble. |
| 18 | + |
| 19 | +!!! info |
| 20 | + If you've used FastAPI, this is `Depends`. Same move, same reason: the function declares what |
| 21 | + it needs, the framework supplies it, and the wiring lives in the type annotation. |
| 22 | + |
| 23 | +### Invisible to the model |
| 24 | + |
| 25 | +Here is the input schema `tools/list` reports for `reserve_book`: |
| 26 | + |
| 27 | +```json |
| 28 | +{ |
| 29 | + "type": "object", |
| 30 | + "properties": { |
| 31 | + "title": {"title": "Title", "type": "string"} |
| 32 | + }, |
| 33 | + "required": ["title"], |
| 34 | + "title": "reserve_bookArguments" |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +One property. Like the `Context` in **The Context**, 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. |
| 39 | + |
| 40 | +That last part is the point. A parameter the model cannot supply is a parameter the model cannot get wrong. |
| 41 | + |
| 42 | +### Try it |
| 43 | + |
| 44 | +Run the server with the MCP Inspector: |
| 45 | + |
| 46 | +```console |
| 47 | +uv run mcp dev server.py |
| 48 | +``` |
| 49 | + |
| 50 | +The form for `reserve_book` has a single `title` field. `stock` is nowhere on it. Call it with `Dune`: |
| 51 | + |
| 52 | +```text |
| 53 | +Reserved 'Dune' (6 copies left). |
| 54 | +``` |
| 55 | + |
| 56 | +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. |
| 57 | + |
| 58 | +!!! tip |
| 59 | + You could just call `check_stock(title)` in the tool body. Declare it as a dependency when the |
| 60 | + value deserves more than a helper call: every tool that needs stock declares the same parameter, |
| 61 | + and the SDK runs the resolver at most once per call, no matter how many declare it. The next |
| 62 | + sections add the rest: resolvers that depend on each other, and resolvers that ask the user. |
| 63 | + |
| 64 | +## Dependencies of dependencies |
| 65 | + |
| 66 | +A resolver can declare its own dependencies, with the same annotation: |
| 67 | + |
| 68 | +```python title="server.py" hl_lines="22 29-30" |
| 69 | +--8<-- "docs_src/dependencies/tutorial002.py" |
| 70 | +``` |
| 71 | + |
| 72 | +* `estimate_delivery` depends on `check_stock`. The SDK runs the graph in order: stock first, then the estimate, then the tool. |
| 73 | +* Both `stock` and `delivery` ultimately need `check_stock`, but it runs **once per call**. One inventory lookup, two consumers. |
| 74 | +* There is nothing to register. The graph *is* the annotations. |
| 75 | + |
| 76 | +!!! check |
| 77 | + Don't take once-per-call on faith. Put a `print` in `check_stock` and call `order_book` from the |
| 78 | + Inspector: one line per call. Two consumers, one lookup. |
| 79 | + |
| 80 | +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. |
| 81 | + |
| 82 | +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. |
| 83 | + |
| 84 | +!!! warning |
| 85 | + On HTTP transports the `Context` includes `ctx.headers`. Headers are **client-supplied input**, |
| 86 | + like any tool argument: fine for a locale or a feature flag, never an identity. Who the caller |
| 87 | + is comes from your authorization layer (**Authorization**), not from a header anyone can set. |
| 88 | + |
| 89 | +!!! tip |
| 90 | + *Once per call* means exactly that: the next `tools/call` runs `check_stock` again. A resource |
| 91 | + that should outlive a request - a database pool, an HTTP client - belongs in **Lifespan**, and |
| 92 | + a resolver can reach it through `ctx.request_context.lifespan_context`. |
| 93 | + |
| 94 | +## Ask when you must |
| 95 | + |
| 96 | +A resolver doesn't have to know the answer. It can return `Elicit(message, Model)` and the SDK asks the user - the **Elicitation** machinery, run for you: |
| 97 | + |
| 98 | +```python title="server.py" hl_lines="26-32 39" |
| 99 | +--8<-- "docs_src/dependencies/tutorial003.py" |
| 100 | +``` |
| 101 | + |
| 102 | +* In stock: `confirm_backorder` returns a `Backorder` directly. **No question, no round-trip.** The user is only interrupted when their answer matters. |
| 103 | +* Out of stock: the SDK sends the elicitation, validates the answer against `Backorder`, and injects it. Your resolver never touches the protocol. |
| 104 | +* 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. |
| 105 | + |
| 106 | +And if the user won't answer at all - declines the question, or cancels it? |
| 107 | + |
| 108 | +!!! check |
| 109 | + Run `order_book` for `Neuromancer` and decline the question. With the annotation written as |
| 110 | + `Annotated[Backorder, Resolve(...)]` the tool body never runs; the call fails with an error |
| 111 | + result the model can read: |
| 112 | + |
| 113 | + ```text |
| 114 | + Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline |
| 115 | + ``` |
| 116 | + |
| 117 | +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** shows that form, and everything else about asking: the schema rules, the three answers, the client's side of the conversation. |
| 118 | + |
| 119 | +## Recap |
| 120 | + |
| 121 | +* `Annotated[T, Resolve(fn)]` on a tool parameter: the SDK runs `fn` and injects its return value. |
| 122 | +* 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. |
| 123 | +* 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 call. |
| 124 | +* Bad graphs fail at registration with `InvalidSignature`, not mid-call. |
| 125 | +* Return `Elicit(message, Model)` to ask the user, only when you have to. Unwrapped annotations abort on decline; `ElicitationResult[T]` lets the tool branch. |
| 126 | + |
| 127 | +Next: what happens when your tool fails, and how to choose who finds out, in **Handling errors**. |
0 commit comments