Rebuild the docs around tested examples; shrink README.v2.md to a pitch (#2978)
This commit is contained in:
parent
4caa41f6d5
commit
411a6d3980
15 changed files with 9510 additions and 2639 deletions
125
docs/tutorial/completions.md
Normal file
125
docs/tutorial/completions.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Completions
|
||||
|
||||
A client building a UI on top of your server wants to autocomplete argument values as the user types: language names, repository names, file paths.
|
||||
|
||||
**Completions** are how your server supplies those suggestions.
|
||||
|
||||
## Something worth completing
|
||||
|
||||
Completions apply to exactly two things: the arguments of a **prompt** and the parameters of a **resource template**. So start with a server that has one of each:
|
||||
|
||||
```python title="server.py" hl_lines="6 12"
|
||||
--8<-- "docs_src/completions/tutorial001.py"
|
||||
```
|
||||
|
||||
Nothing here is about completions yet.
|
||||
|
||||
* `review_code` takes a `language`. A user shouldn't have to guess which spellings you accept.
|
||||
* `github_repo` takes an `owner` and a `repo`. Free-text boxes for both make a bad form.
|
||||
|
||||
## The completion handler
|
||||
|
||||
Add **one** function decorated with `@mcp.completion()`:
|
||||
|
||||
```python title="server.py" hl_lines="22-30"
|
||||
--8<-- "docs_src/completions/tutorial002.py"
|
||||
```
|
||||
|
||||
* There is one handler per server. Every completion request lands here, and you branch on what's being completed.
|
||||
* It must be `async def`: the SDK awaits it.
|
||||
* It receives three arguments:
|
||||
* `ref`: *which* prompt or resource template, as a `PromptReference` or a `ResourceTemplateReference`. `isinstance` is how you tell them apart.
|
||||
* `argument`: `argument.name` is the argument being completed, `argument.value` is what the user has typed so far.
|
||||
* `context`: the arguments already resolved. Ignore it for now.
|
||||
* You return a `Completion(values=[...])`, or `None` when you have nothing to offer.
|
||||
|
||||
!!! tip
|
||||
`argument.value` is the prefix the user has typed. The SDK does **not** filter for you: whatever
|
||||
you put in `values` is what the UI shows. The `startswith` is yours to write.
|
||||
|
||||
### Try it
|
||||
|
||||
Drive it with the in-memory `Client`, the same one you use in **Testing**. Call
|
||||
`client.complete()` with `ref=PromptReference(name="review_code")` and
|
||||
`argument={"name": "language", "value": "py"}`:
|
||||
|
||||
```python
|
||||
result.completion.values # ['python']
|
||||
```
|
||||
|
||||
* `ref` is the same reference type your handler receives.
|
||||
* `argument` is a plain dict with exactly two keys, `name` and `value`.
|
||||
|
||||
Send an empty `value` and you get the whole list back. `lang.startswith("")` is true for every language:
|
||||
|
||||
```python
|
||||
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
|
||||
```
|
||||
|
||||
Ask about `code` (an argument your handler doesn't recognise) and it returns `None`, which the SDK turns into an empty list:
|
||||
|
||||
```python
|
||||
result.completion.values # []
|
||||
```
|
||||
|
||||
`None` means *"no suggestions"*, never an error. A UI falls back to a plain text box.
|
||||
|
||||
## A capability you never declared
|
||||
|
||||
Registering the handler is the declaration. Connect a client and look:
|
||||
|
||||
```python
|
||||
client.server_capabilities.completions # CompletionsCapability()
|
||||
```
|
||||
|
||||
You didn't list `completions` anywhere. The SDK saw the handler and advertised it during the handshake. Every *optional* capability works this way: the handler is the declaration. (The three primitives are not optional: `MCPServer` always declares those, handlers or not.)
|
||||
|
||||
!!! check
|
||||
Go back to the first `server.py` (the one with no handler) and ask it anyway. The call fails
|
||||
with a JSON-RPC error:
|
||||
|
||||
```text
|
||||
Method not found
|
||||
```
|
||||
|
||||
And `client.server_capabilities.completions` is `None`. That's the point of the capability: a
|
||||
well-behaved client checks it and never sends the request you can't answer.
|
||||
|
||||
## Dependent arguments
|
||||
|
||||
`github://repos/{owner}/{repo}` has two parameters, and the useful values for `repo` depend on which `owner` was picked first.
|
||||
|
||||
That's what `context` is for. It carries the arguments the user has **already resolved**:
|
||||
|
||||
```python title="server.py" hl_lines="9-12 35-39"
|
||||
--8<-- "docs_src/completions/tutorial003.py"
|
||||
```
|
||||
|
||||
* The new branch fires for the template's `repo` parameter.
|
||||
* `context.arguments` is a `dict[str, str] | None` of the values picked so far (here, `owner`).
|
||||
* No `owner` yet means no sensible suggestions, so the handler returns `None`.
|
||||
|
||||
The client sends those resolved values with `context_arguments=`. This time `ref` is a
|
||||
`ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`. Ask for `repo` with an
|
||||
empty `value` and pass `context_arguments={"owner": "modelcontextprotocol"}`:
|
||||
|
||||
```python
|
||||
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
|
||||
```
|
||||
|
||||
Drop `context_arguments=` and the same call returns `[]`. The handler can't know which repos to offer until it knows the owner.
|
||||
|
||||
!!! info
|
||||
`Completion` also takes `total=` and `has_more=`. Set them when `values` is a slice of a longer
|
||||
list, so a UI can show *"and 200 more"*. Most handlers never need them.
|
||||
|
||||
## Recap
|
||||
|
||||
* Completions are suggestions for **prompt arguments** and **resource template parameters**. Nothing else.
|
||||
* `@mcp.completion()` registers the one handler. It's `async def (ref, argument, context) -> Completion | None`.
|
||||
* Branch on `isinstance(ref, ...)` and on `argument.name`. Filter by `argument.value` yourself.
|
||||
* `None` becomes an empty list. It is never an error.
|
||||
* `context.arguments` holds the already-resolved values; the client supplies them as `context_arguments=`.
|
||||
* The `completions` capability appears the moment you register the handler. Without it, the request is `Method not found`.
|
||||
|
||||
Suggestions help *before* a tool runs. To ask the user a question in the *middle* of one, you want **Elicitation**.
|
||||
126
docs/tutorial/context.md
Normal file
126
docs/tutorial/context.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# The Context
|
||||
|
||||
A tool's arguments come from the model. Everything else (the request you are serving, the server you live in, a way to talk back to the client) comes from one object: the **`Context`**.
|
||||
|
||||
You don't construct it and you don't configure it. You ask for it.
|
||||
|
||||
## Ask for it
|
||||
|
||||
Add a parameter annotated with `Context` to any tool:
|
||||
|
||||
```python title="server.py" hl_lines="2 8"
|
||||
--8<-- "docs_src/context/tutorial001.py"
|
||||
```
|
||||
|
||||
* The SDK builds a fresh `Context` for every request and passes it in.
|
||||
* The parameter **name doesn't matter**. `ctx`, `context`, `c`: the SDK finds it by its annotation.
|
||||
* Resources and prompts can declare one too, the same way.
|
||||
* `ctx.request_id` is the id of the request your function is serving right now.
|
||||
|
||||
!!! info
|
||||
If you've used FastAPI, you've seen this move: declare a parameter with the framework's own type
|
||||
(`Request` there, `Context` here) and the framework supplies it. Nothing to register, nothing to
|
||||
configure: the type annotation is the whole mechanism.
|
||||
|
||||
### Invisible to the model
|
||||
|
||||
This is the part to internalise. Here is the input schema `tools/list` reports for `search_books`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
One property. `ctx` is not an argument: it never appears in the schema, the model is never told about it, and no client can fill it in. It's a contract between you and the SDK, invisible on the wire.
|
||||
|
||||
### Try it
|
||||
|
||||
Run the server with the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
The form for `search_books` has a single `query` field. Call it with `dune`:
|
||||
|
||||
```text
|
||||
[request 3] Found 3 books matching 'dune'.
|
||||
```
|
||||
|
||||
The number is whichever request this happened to be. Call the tool again and it changes: every request gets its own `Context`.
|
||||
|
||||
## What it gives you
|
||||
|
||||
The injected object is small. Besides `request_id`:
|
||||
|
||||
* `await ctx.read_resource(uri)`: read one of the server's **own** resources from inside a tool. The next section.
|
||||
* `await ctx.report_progress(progress, total, message)`: stream progress back to the caller during a long call. The whole story is in **Progress**.
|
||||
* `await ctx.elicit(message, schema)` and `await ctx.elicit_url(...)`: pause the tool and ask the user a question. That's **Elicitation**.
|
||||
* `ctx.session`: the server's side of the conversation with this client. Notifications you send to the client live here; the last section uses it.
|
||||
* `ctx.request_context`: the raw per-request record. The field you'll reach for is `lifespan_context`, the object your startup code yielded (see **Lifespan**).
|
||||
|
||||
Logging is deliberately not on that list. A server logs with Python's `logging` module, like any other Python program. **Logging** is the short chapter on why.
|
||||
|
||||
!!! tip
|
||||
Injection only happens for the function you registered. A helper that your tool calls doesn't get
|
||||
its own `Context`; pass `ctx` down as an ordinary argument. There is no ambient
|
||||
"current context" to fetch from somewhere else.
|
||||
|
||||
## Read your own resources
|
||||
|
||||
A server's resources aren't only for clients. A tool can read them too:
|
||||
|
||||
```python title="server.py" hl_lines="16"
|
||||
--8<-- "docs_src/context/tutorial002.py"
|
||||
```
|
||||
|
||||
`ctx.read_resource` resolves the URI through the same registry that serves `resources/read`, so a tool gets what a client would get: an iterable of `ReadResourceContents`, one per content block. For this URI there is one:
|
||||
|
||||
```python
|
||||
contents.content # 'fiction, non-fiction, poetry'
|
||||
contents.mime_type # 'text/plain'
|
||||
```
|
||||
|
||||
* `content` is exactly what `genres()` returned. One source of truth: the client browses the resource, your tools consume it, nobody copies the string.
|
||||
* `describe_catalog`'s only parameter is the `Context`, so its input schema has **no properties at all**. The model calls it with `{}`.
|
||||
|
||||
## Tell the client the list changed
|
||||
|
||||
What a server offers is not fixed at import time. Register a tool at runtime, then tell the client:
|
||||
|
||||
```python title="server.py" hl_lines="15-16"
|
||||
--8<-- "docs_src/context/tutorial003.py"
|
||||
```
|
||||
|
||||
* `mcp.add_tool(recommend_book)` registers a plain function as a tool: name, description and schema derived exactly as `@mcp.tool()` would have.
|
||||
* `await ctx.session.send_tool_list_changed()` sends `notifications/tools/list_changed`. A client that receives it calls `tools/list` again and sees `recommend_book`.
|
||||
|
||||
The siblings are `send_resource_list_changed()`, `send_prompt_list_changed()`, and `send_resource_updated(uri)` for a change to one specific resource.
|
||||
|
||||
!!! check
|
||||
Before anyone runs `enable_recommendations`, the tool you are promising does not exist. Call it
|
||||
anyway and the result is an error the model can read:
|
||||
|
||||
```text
|
||||
Unknown tool: recommend_book
|
||||
```
|
||||
|
||||
Run `enable_recommendations`, and the very same call succeeds. The tool list is genuinely
|
||||
dynamic: `tools/list` reflects whatever is registered *right now*.
|
||||
|
||||
## Recap
|
||||
|
||||
* Annotate a parameter with `Context` (in a tool, a resource, or a prompt) and the SDK injects it. The name is yours.
|
||||
* It is invisible to the model: the input schema only ever contains your real arguments.
|
||||
* `ctx.request_id` identifies the request; `ctx.request_context.lifespan_context` is what your startup yielded.
|
||||
* `await ctx.read_resource(uri)` lets a tool read the server's own resources.
|
||||
* `ctx.session` is the channel back to the client: `send_tool_list_changed()` and its siblings tell it to re-fetch a list you changed.
|
||||
* Progress reporting and elicitation also start at `Context`; each has its own chapter.
|
||||
|
||||
Next: what happens when your tool fails, and how to choose who finds out, in **Handling errors**.
|
||||
153
docs/tutorial/elicitation.md
Normal file
153
docs/tutorial/elicitation.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Elicitation
|
||||
|
||||
A tool that is halfway through its job and missing one answer doesn't have to fail.
|
||||
|
||||
**Elicitation** lets it ask. In the middle of a tool call the server sends the client a question, the client puts it to the user, and the answer comes back into the same function call.
|
||||
|
||||
There are two modes:
|
||||
|
||||
* **Form mode**: you need a value (a confirmation, a date, a quantity). You describe the fields, the client renders the form.
|
||||
* **URL mode**: you need the user to go somewhere else (an OAuth consent screen, a payment page). Nothing they do there passes through the protocol.
|
||||
|
||||
## Ask with a form
|
||||
|
||||
`ctx.elicit()` takes a message and a Pydantic model:
|
||||
|
||||
```python title="server.py" hl_lines="9-11 20-23 25"
|
||||
--8<-- "docs_src/elicitation/tutorial001.py"
|
||||
```
|
||||
|
||||
* The **`Context`** parameter is what gives you `ctx.elicit`; any tool can take one. That object has its own chapter: **The Context**.
|
||||
* `AlternativeDate` is the **schema** of the answer you want.
|
||||
* The tool is `async def`. It has to be: it stops in the middle and waits for a person.
|
||||
* On any other date the tool returns straight away. It only asks when it has to.
|
||||
* The date the user accepts goes back through `book_table` itself. An answer is input like any other: an alternative that is also fully booked gets asked about again, not confirmed blind.
|
||||
|
||||
### What the client receives
|
||||
|
||||
The client gets your message and, next to it, a JSON Schema generated from the model:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"accept_alternative": {
|
||||
"description": "Try another date?",
|
||||
"title": "Accept Alternative",
|
||||
"type": "boolean"
|
||||
},
|
||||
"date": {
|
||||
"default": "2025-12-26",
|
||||
"description": "Alternative date (YYYY-MM-DD)",
|
||||
"title": "Date",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["accept_alternative"],
|
||||
"title": "AlternativeDate",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
That schema is the form. `Field(description=...)` is the label; a default pre-fills the input and makes the field optional. It's the same Pydantic-to-JSON-Schema machinery you already used for a tool's arguments in **Tools**.
|
||||
|
||||
!!! warning
|
||||
An elicitation schema is not as expressive as a tool's input schema. Flat, primitive fields
|
||||
only: `str`, `int`, `float`, `bool`, or a `Literal` of strings (it becomes an `enum`).
|
||||
Put a model inside the model and `ctx.elicit` raises before anything is sent to the client:
|
||||
|
||||
```text
|
||||
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
|
||||
```
|
||||
|
||||
You are interrupting a person mid-task. If the answer needs nesting, it should have been an
|
||||
argument to the tool.
|
||||
|
||||
### The three answers
|
||||
|
||||
`result.action` tells you what the user did, and there are exactly three possibilities:
|
||||
|
||||
* `"accept"`: they submitted the form. `result.data` is an `AlternativeDate` instance, already validated.
|
||||
* `"decline"`: they said no.
|
||||
* `"cancel"`: they dismissed the question without choosing.
|
||||
|
||||
`result.data` only exists on `"accept"`, which is why the example checks `result.action` first. Your type checker enforces the order: after `result.action == "accept"`, `result.data` is an `AlternativeDate`; before it, there is no `.data` at all.
|
||||
|
||||
A refusal is not an error. The tool decides what declining means (here, no booking) and answers the model normally.
|
||||
|
||||
!!! tip
|
||||
The answer is validated against your model before your code sees it. A client that sends
|
||||
`"maybe"` for a `bool` doesn't corrupt your booking: the call fails with the
|
||||
`ValidationError`, your `if` never runs.
|
||||
|
||||
## Send the user to a URL
|
||||
|
||||
Some things must not go through the model or the client: credentials, card numbers, OAuth consent. For those you don't ask for data; you ask the user to go somewhere:
|
||||
|
||||
```python title="server.py" hl_lines="10-14 23"
|
||||
--8<-- "docs_src/elicitation/tutorial002.py"
|
||||
```
|
||||
|
||||
* `ctx.elicit_url()` takes the message, the **URL** to visit, and an `elicitation_id` you choose: any string that identifies this elicitation within your server.
|
||||
* The result has an action and nothing else. `"accept"` means the user agreed to open the URL, **not** that they finished what's on the other side.
|
||||
* The payment happens out of band, between the user's browser and your payment provider. No content ever comes back through MCP.
|
||||
|
||||
Look at the second tool. When your server learns the out-of-band flow finished (a webhook, a poll; here it's modelled as a second tool), `ctx.session.send_elicit_complete(...)` sends `notifications/elicitation/complete` with the same `elicitation_id`. That is how the client knows it can stop showing *"waiting for payment..."*. Without it, the client can only guess.
|
||||
|
||||
## The client side
|
||||
|
||||
Servers ask. Clients answer by passing an **`elicitation_callback`** to `Client(...)`:
|
||||
|
||||
```python title="client.py" hl_lines="7-8 19"
|
||||
--8<-- "docs_src/elicitation/tutorial003.py"
|
||||
```
|
||||
|
||||
* One callback handles both modes. `params` is a union of `ElicitRequestFormParams` and `ElicitRequestURLParams`; `isinstance` is the branch.
|
||||
* For a URL, you show `params.url` to the user and return the action they chose. Never any `content`.
|
||||
* For a form, a real application renders `params.requested_schema` and returns the user's input as `content`. This one always says yes with a canned answer, which is exactly the callback you want in a test.
|
||||
* Passing the callback is also the **capability declaration**: it's how the server learns this client can be asked. The other things a client can answer for a server live in **Client callbacks**.
|
||||
|
||||
!!! info
|
||||
Elicitation is a request from the *server* to the *client*, and those only exist on a
|
||||
classic-handshake session, which is why this client passes `mode="legacy"`.
|
||||
On a **2026-07-28** connection a tool asks by *returning* the question from the call
|
||||
instead; that flow is **Multi-round-trip requests**.
|
||||
|
||||
### Try it
|
||||
|
||||
Start the form-mode `server.py` (the first one on this page) on Streamable HTTP (**Running your server** has the one-liner), then run the client's `main()` and ask `book_table` for Christmas day.
|
||||
|
||||
The callback prints the question it was sent:
|
||||
|
||||
```text
|
||||
No tables for 2 on 2025-12-25. Would you like to try another date?
|
||||
```
|
||||
|
||||
It answers with `{"accept_alternative": True, "date": "2025-12-27"}`, and the tool, which has been waiting inside `await ctx.elicit(...)` this whole time, finishes the booking:
|
||||
|
||||
```text
|
||||
Booked a table for 2 on 2025-12-27.
|
||||
```
|
||||
|
||||
Now swap in the URL-mode `server.py` and point the same `main()` at `pay_deposit`: the same callback takes the other branch, prints the payment link, and the tool comes back with *"Complete the payment in your browser."* One round trip, mid-call, in both directions.
|
||||
|
||||
!!! check
|
||||
Now remove `elicitation_callback=` from the `Client` and call `book_table` for Christmas day
|
||||
again. The whole call fails with a protocol error:
|
||||
|
||||
```text
|
||||
Elicitation not supported
|
||||
```
|
||||
|
||||
A client that registered no callback never declared the `elicitation` capability, so there is
|
||||
nobody to ask. Your tool didn't get a `"decline"`; it got an exception. Design for it: every
|
||||
elicitation needs a sensible answer to "what if I can't ask?".
|
||||
|
||||
## Recap
|
||||
|
||||
* `await ctx.elicit(message, schema=Model)` asks mid-call; your tool resumes with the answer.
|
||||
* The schema is a flat Pydantic model: primitive fields only, validated on the way back.
|
||||
* `result.action` is `"accept"`, `"decline"` or `"cancel"`; `result.data` exists only on accept.
|
||||
* `await ctx.elicit_url(message, url, elicitation_id)` is for everything that must not pass through the model; `ctx.session.send_elicit_complete(elicitation_id)` says the out-of-band part is done.
|
||||
* The client answers with one `elicitation_callback`, branching on the params type; registering it is what declares the capability.
|
||||
|
||||
A tool that can ask is good. A tool that says how far along it is (**Progress**) is next.
|
||||
139
docs/tutorial/first-steps.md
Normal file
139
docs/tutorial/first-steps.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# First steps
|
||||
|
||||
On the landing page you wrote a server, ran it, and called a tool.
|
||||
|
||||
Now do it again, slowly, with all three things a server can expose, and the names for everything you just saw.
|
||||
|
||||
## Host, client, and server
|
||||
|
||||
Three words you'll see on every page from here on:
|
||||
|
||||
* A **host** is the LLM application: Claude, an IDE, an agent runtime. It's the thing the user is talking to.
|
||||
* A **client** lives inside the host and speaks MCP. The host runs one client per server it's connected to.
|
||||
* A **server** is what you build with this SDK. It exposes things to clients. It never talks to the model directly.
|
||||
|
||||
You write the server. Hosts are someone else's product. The SDK also gives you a `Client`. You'll use it to test your servers, and it shows up later in this chapter.
|
||||
|
||||
## The three primitives
|
||||
|
||||
A server exposes exactly three kinds of thing. What separates them is **who decides to use them**:
|
||||
|
||||
| Primitive | Controlled by | What it is | Example |
|
||||
|---------------|-----------------|-----------------------------------------------------|------------------------------------|
|
||||
| **Tools** | The model | A function the model calls to take an action | An API call, a database write |
|
||||
| **Resources** | The application | Data the host loads into the model's context | A file's contents, an API response |
|
||||
| **Prompts** | The user | A reusable message template the user invokes by name | A slash command, a menu entry |
|
||||
|
||||
"Controlled by" is the whole point of the split. A tool runs because the **model** decided to call it. A resource is attached because the **application** decided the model needed it. A prompt runs because the **user** picked it.
|
||||
|
||||
!!! info
|
||||
If you've built a web API you already have most of the intuition: a **resource** is a `GET`
|
||||
(it loads data and changes nothing) and a **tool** is a `POST` (it does work and may have
|
||||
side effects). A **prompt** has no HTTP analogue; it's closer to a saved query the user runs
|
||||
by name.
|
||||
|
||||
## One server, all three
|
||||
|
||||
```python title="server.py" hl_lines="6 12 18"
|
||||
--8<-- "docs_src/first_steps/tutorial001.py"
|
||||
```
|
||||
|
||||
Three plain functions, three decorators. Each decorator is the entire registration:
|
||||
|
||||
* `@mcp.tool()` makes `add` a **tool**.
|
||||
* `@mcp.resource("greeting://{name}")` makes `greeting` a **resource template**: the `{name}` in the URI is the function's parameter.
|
||||
* `@mcp.prompt()` makes `summarize` a **prompt**. The string it returns becomes a user message.
|
||||
|
||||
Everything else (the name, the description, the argument schema) the SDK reads from the function itself: its name, its docstring, its type hints. You never declared any of it separately.
|
||||
|
||||
!!! tip
|
||||
The two halves of the SDK have two import paths: `from mcp import Client` and
|
||||
`from mcp.server import MCPServer`. There is no `from mcp import MCPServer`.
|
||||
|
||||
### Try it
|
||||
|
||||
Run it with the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Open the URL it prints. The Inspector has one tab per primitive; walk through them in order.
|
||||
|
||||
**Tools.** One entry: `add`, described as *Add two numbers.* The form has a required integer field for `a` and another for `b`. Fill them in, call it, and the result is `3`. The Inspector built that form from `a: int, b: int`. So does every other client.
|
||||
|
||||
**Resources.** The *Resources* list is empty. `greeting` is under **Resource Templates**, because `greeting://{name}` has a parameter: there is no single resource to list until someone supplies a `name`. Give it `World` and read it:
|
||||
|
||||
```text
|
||||
Hello, World!
|
||||
```
|
||||
|
||||
**Prompts.** One entry: `summarize`, with a single required `text` argument. Get it with some text and you receive one message with `role: user` and your rendered string as the content. That's all a prompt is: a function that builds messages.
|
||||
|
||||
The Inspector ran your server over **stdio**, one of the transports an MCP server can speak. You don't pick one yet; **Running your server** is the chapter for that.
|
||||
|
||||
## Capabilities
|
||||
|
||||
You saw three tabs in the Inspector. How did it know there were three?
|
||||
|
||||
When a client connects, the server declares its **capabilities**: which families of requests it will answer. The client uses that declaration to decide what to even ask for. You never wrote it; `MCPServer` declares it for you.
|
||||
|
||||
Look at it yourself. The SDK's `Client` accepts the server object directly and connects to it **in memory** (no subprocess, no port):
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
{'prompts': {'list_changed': False}, 'resources': {'subscribe': False, 'list_changed': False}, 'tools': {'list_changed': False}}
|
||||
```
|
||||
|
||||
That dictionary is the server's half of the handshake:
|
||||
|
||||
| Capability | The client may now call |
|
||||
|-------------|------------------------------------------------------------|
|
||||
| `tools` | `tools/list`, `tools/call` |
|
||||
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
|
||||
| `prompts` | `prompts/list`, `prompts/get` |
|
||||
|
||||
`MCPServer` serves all three primitives, so all three are always declared.
|
||||
|
||||
Notice what isn't there. `completions` (argument autocomplete for resource templates and prompts) needs a handler you write, this server doesn't have one, so the capability is absent and a well-behaved client won't ask. That's the rule for everything optional: register the thing and the capability appears; **Completions** proves it.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` is the same in-memory client every example in this tutorial is tested with, and
|
||||
it's how you'll test yours. It gets a whole chapter: **Testing**.
|
||||
|
||||
## What you did not write
|
||||
|
||||
Look back over this page. You wrote three small Python functions. You did **not** write:
|
||||
|
||||
* A JSON Schema. `a: int, b: int` *is* the schema for `add`.
|
||||
* A request handler. `tools/list`, `resources/read`, `prompts/get`: all served for you.
|
||||
* A capability declaration. `MCPServer` made it for you.
|
||||
* A line of protocol. The handshake, the version negotiation, the JSON-RPC framing: all of it happened inside `mcp dev` and `Client(mcp)`, and you never saw it.
|
||||
|
||||
That ratio is the whole point of the SDK.
|
||||
|
||||
## Recap
|
||||
|
||||
* A **host** is the LLM app, a **client** is its MCP-speaking half, a **server** is what you build.
|
||||
* Tools are **model**-controlled, resources are **application**-controlled, prompts are **user**-controlled.
|
||||
* One decorator per primitive: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Name, description, and schema come from the function.
|
||||
* A URI with a `{param}` makes a resource **template**, listed separately from concrete resources.
|
||||
* The server's **capabilities** are declared for you, and a client only asks for what a server declares.
|
||||
* `Client(mcp)` connects to the server object in memory: your test harness from day one.
|
||||
|
||||
Each primitive now gets its own chapter, starting with the one the model drives: **Tools**.
|
||||
132
docs/tutorial/handling-errors.md
Normal file
132
docs/tutorial/handling-errors.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Handling errors
|
||||
|
||||
A tool can fail in two ways, and the SDK treats them very differently.
|
||||
|
||||
Raise an ordinary exception and the **model** sees it. Raise `MCPError` and the **protocol** sees it.
|
||||
|
||||
This chapter is about choosing.
|
||||
|
||||
## An error the model can fix
|
||||
|
||||
Take a tool that looks something up, and let the lookup miss:
|
||||
|
||||
```python title="server.py" hl_lines="11-12"
|
||||
--8<-- "docs_src/handling_errors/tutorial001.py"
|
||||
```
|
||||
|
||||
There is nothing MCP about those two lines. `get_author` raises a plain `ValueError`, the way any Python function would.
|
||||
|
||||
Call it with a title that isn't in the catalog and look at the result:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
* The request **succeeded**. There is a result; nothing was raised at the caller.
|
||||
* `is_error` is `True`, and your exception's message (prefixed with the tool name) is in `content`, exactly where the model reads.
|
||||
* `structured_content` is `None`. A failed call has no return value to structure.
|
||||
|
||||
This is a **tool error**, and it is the default for *any* exception your tool raises. It is also almost always what you want.
|
||||
|
||||
The model is the one calling your tool. It picked the arguments. So a tool error is a turn in the conversation: the model reads *"No book titled 'Nothing' in the catalog."*, realises it guessed the title wrong, and calls again with a better one. You wrote one `raise` and got a self-correcting agent.
|
||||
|
||||
!!! tip
|
||||
Never `return` an error message from a tool. A returned string has `is_error=False`, so to the
|
||||
model (and to every client UI) it looks like the tool worked and that string was the answer.
|
||||
`raise`. The flag is the signal.
|
||||
|
||||
## An error the model cannot fix
|
||||
|
||||
Now swap `ValueError` for `MCPError`.
|
||||
|
||||
```python title="server.py" hl_lines="1 3 15"
|
||||
--8<-- "docs_src/handling_errors/tutorial002.py"
|
||||
```
|
||||
|
||||
`MCPError` is the SDK's **protocol error**. It is the one exception the tool wrapper does *not* catch: it propagates, and the whole `tools/call` request fails with a JSON-RPC error instead of a result.
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "No book titled 'Nothing' in the catalog."
|
||||
}
|
||||
```
|
||||
|
||||
* There is **no result**. No `content`, no `is_error`: nothing for the model to read.
|
||||
* The **host** application gets the error instead, the same way it would if the tool didn't exist at all.
|
||||
* `code`, `message`, and `data` arrive intact. `INVALID_PARAMS` is `-32602`; `mcp_types` exports it and the other JSON-RPC error codes (`INVALID_REQUEST`, `INTERNAL_ERROR`, ...) as constants so you never type a magic number.
|
||||
|
||||
!!! check
|
||||
Same lookup, same miss, but now the call *raises* on the client side instead of returning:
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
|
||||
```
|
||||
|
||||
The first version handed the model a sentence it could react to. This one hands it nothing.
|
||||
For `get_author` that is strictly worse, which is the point of the next section.
|
||||
|
||||
## Which one to raise
|
||||
|
||||
The two paths answer two different questions.
|
||||
|
||||
* **Raise any exception** for a failure of *execution*: the thing your tool tried to do didn't work. The model chose the call, so the model should see the consequence and get a chance to recover. A misspelled title, an upstream API that timed out, a row that doesn't exist: all tool errors.
|
||||
* **Raise `MCPError`** when the *request itself* should be rejected: the client is missing a capability your tool depends on, the server isn't in a state to serve anyone, the caller skipped a required step. No retry from the model fixes any of those, so there is nothing to gain from handing it the message.
|
||||
|
||||
One question decides it: **could a smarter model have avoided this?** Yes -> ordinary exception. No -> `MCPError`.
|
||||
|
||||
By that test, the second version of `get_author` made the wrong choice: a better title fixes it, so the model deserved to see the message. It's there to show you the mechanism, not to recommend it.
|
||||
|
||||
!!! info
|
||||
`MCPError` lives at `from mcp import MCPError` and takes `code`, `message`, and an optional
|
||||
`data` payload. Whatever you put in them is what the client receives: the SDK forwards a raised
|
||||
`MCPError` verbatim instead of sanitising it.
|
||||
|
||||
## A resource that doesn't exist
|
||||
|
||||
Resources draw the same line, and ship one named exception for the common case.
|
||||
|
||||
```python title="server.py" hl_lines="2 13"
|
||||
--8<-- "docs_src/handling_errors/tutorial003.py"
|
||||
```
|
||||
|
||||
`books://{title}` is a **template**. It matches *any* title, so "the URI is well-formed" and "the book exists" are two different questions, and only your function can answer the second one.
|
||||
|
||||
When it can't, raise `ResourceNotFoundError`. The SDK turns it into the protocol error the spec assigns to a missing resource: `-32602` with the requested URI in `data`, so the client knows *which* read failed.
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "No book titled 'Nothing' in the catalog.",
|
||||
"data": {"uri": "books://Nothing"}
|
||||
}
|
||||
```
|
||||
|
||||
Notice there is no `is_error=True` half-result here. A resource read either returns contents or fails: resources have only the protocol path. Templates and everything else about resources live in **Resources**.
|
||||
|
||||
## Errors you never raise
|
||||
|
||||
A bad argument never reaches your function.
|
||||
|
||||
Send `get_author` a `title` that isn't a string and the SDK rejects it against the input schema **before** calling you, as the same kind of `is_error=True` tool error the model can read and correct. You saw this in **Tools** with `Field(le=50)`.
|
||||
|
||||
It means a whole class of `raise` statements you don't write: don't re-validate your own type hints.
|
||||
|
||||
!!! info
|
||||
Everything on this page is what a **client** sees, and the in-memory `Client` you'll write
|
||||
tests with sees exactly the same thing. Even `raise_exceptions=True` doesn't turn a tool error
|
||||
back into a traceback: by the time that flag could act, your exception is already the
|
||||
`is_error=True` result. Assert on the result. **Testing** covers the pattern.
|
||||
|
||||
## Recap
|
||||
|
||||
* Raise **any exception** in a tool -> the call returns `is_error=True` with your message in `content`. The model reads it and can retry. This is the default.
|
||||
* Raise **`MCPError`** -> the call itself fails with a JSON-RPC error. The model sees nothing; the host deals with it. `code`, `message`, and `data` survive intact.
|
||||
* The deciding question: *could a smarter model have avoided this?* Yes -> exception. No -> `MCPError`.
|
||||
* `ResourceNotFoundError` from a resource handler -> the protocol's `-32602`, with the URI in `data`.
|
||||
* Bad arguments are rejected against the schema before your function runs; you don't `raise` for those.
|
||||
* `from mcp import MCPError`; the error-code constants come from `mcp_types`.
|
||||
|
||||
Errors handled. Next: the things your server sets up once, before the first call ever arrives, the **Lifespan**.
|
||||
51
docs/tutorial/index.md
Normal file
51
docs/tutorial/index.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Tutorial - User Guide
|
||||
|
||||
This tutorial shows you how to use the MCP Python SDK, step by step.
|
||||
|
||||
Each section gradually builds on the previous ones, but it's written so you can go straight to any specific section to solve a specific problem. It also works as a future reference: you can come back to exactly the part you need.
|
||||
|
||||
## Run the code
|
||||
|
||||
All the code blocks can be copied and used directly: they are complete, working files.
|
||||
|
||||
To follow along, paste a block into a `server.py` and open it in the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
It is **HIGHLY encouraged** that you write (or copy) the code, edit it, and run it locally. Using it in your own editor is what really shows you the point: how little you write, the autocompletion, the type checks catching mistakes before you run anything.
|
||||
|
||||
## You will not be guessing
|
||||
|
||||
Every example in this tutorial is a complete file under [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) in the SDK's own repository, and every one of them is exercised by the SDK's test suite through an **in-memory client**:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result.structured_content == {"result": 3}
|
||||
```
|
||||
|
||||
No subprocess, no port, no transport. `Client(mcp)` connects to the server object directly.
|
||||
|
||||
If a change to the SDK breaks an example on one of these pages, CI goes red before the page does. The code you read here is the code that runs.
|
||||
|
||||
You'll use this yourself in the [Testing](testing.md) chapter; it's how you test your own servers, too.
|
||||
|
||||
## Install the SDK
|
||||
|
||||
If you haven't yet, [install the SDK](../installation.md) first.
|
||||
|
||||
## Advanced User Guide
|
||||
|
||||
There is also an **Advanced User Guide** you can read after this one.
|
||||
|
||||
It builds on this tutorial, uses the same concepts, and teaches you the extra things: the low-level `Server`, middleware, authorization, the 2026-07-28 protocol negotiation. But you should read this first: everything in the Advanced guide assumes you know the basics.
|
||||
102
docs/tutorial/lifespan.md
Normal file
102
docs/tutorial/lifespan.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Lifespan
|
||||
|
||||
Most real servers hold something for their whole life: a database pool, an HTTP client, a loaded model.
|
||||
|
||||
You don't want to build it on every call, and you do want to close it cleanly. That's what the **lifespan** is for.
|
||||
|
||||
## A typed lifespan
|
||||
|
||||
A lifespan is an `@asynccontextmanager` that receives the server and `yield`s **one object**. Whatever you yield is available to every handler for as long as the server runs.
|
||||
|
||||
```python title="server.py" hl_lines="25-31 34 38 40"
|
||||
--8<-- "docs_src/lifespan/tutorial001.py"
|
||||
```
|
||||
|
||||
Read it bottom-up:
|
||||
|
||||
* `app_lifespan` connects the `Database` **before** the `yield` and disconnects it **after**, in a `finally`. That's startup and shutdown.
|
||||
* It yields an `AppContext`, a plain dataclass holding the things you set up. One field today, ten tomorrow.
|
||||
* `MCPServer("Bookshop", lifespan=app_lifespan)` is the whole wiring.
|
||||
* Inside the tool, the yielded object is `ctx.request_context.lifespan_context`.
|
||||
|
||||
The lifespan runs **once**. It is entered when the server starts (before the first request) and exited when the server stops. Every request in between shares the same `AppContext`.
|
||||
|
||||
!!! info
|
||||
If you've written a FastAPI `lifespan`, you already know this. Same decorator, same `yield`, same `finally`.
|
||||
|
||||
### What the model sees
|
||||
|
||||
Nothing new. `ctx` is a **Context** parameter, so the SDK injects it and it never reaches the input schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"genre": {"title": "Genre", "type": "string"}
|
||||
},
|
||||
"required": ["genre"],
|
||||
"title": "count_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
`genre` is the only argument the model can pass. The lifespan is your server's business.
|
||||
|
||||
`@mcp.resource()` and `@mcp.prompt()` functions can take a `ctx` parameter too, written as a bare `Context` for a reason the next section gets to. Everything `ctx` carries is in **The Context**.
|
||||
|
||||
### It really is typed
|
||||
|
||||
Look at the annotation again: `ctx: Context[AppContext]`.
|
||||
|
||||
That one type parameter is why `ctx.request_context.lifespan_context` **is** an `AppContext` to your type checker. `.db` autocompletes; `.dbb` is an error before you ever run the server.
|
||||
|
||||
Write a bare `Context` instead and `lifespan_context` is typed as `dict[str, Any]`: the type checker has no way to know what your lifespan yielded. The object is still there at runtime; you've lost the help.
|
||||
|
||||
!!! warning
|
||||
`Context[AppContext]` is a **tool-only** spelling. Put it on an `@mcp.resource()` or
|
||||
`@mcp.prompt()` function and every call to that handler fails. The client gets an error back,
|
||||
and the server log shows why:
|
||||
|
||||
```text
|
||||
Context is not available outside of a request
|
||||
```
|
||||
|
||||
In resources and prompts, write the bare `ctx: Context`. The object your lifespan yielded is
|
||||
still `ctx.request_context.lifespan_context` at runtime; you give up the type parameter, not
|
||||
the object.
|
||||
|
||||
!!! tip
|
||||
There is always a lifespan. If you don't pass one, the SDK's default yields an empty `dict`,
|
||||
so `ctx.request_context.lifespan_context` is `{}`, never `None`. That default is also why a
|
||||
bare `Context` types it as `dict[str, Any]`.
|
||||
|
||||
## Watch it happen
|
||||
|
||||
"Startup runs before the first request" is the kind of sentence you should not have to take on faith.
|
||||
|
||||
Strip the server down to the lifecycle: give `Database` a `connected` flag, flip it in `connect()` and `disconnect()`, and add a tool that reports it.
|
||||
|
||||
```python title="server.py" hl_lines="11 14 17 25 44"
|
||||
--8<-- "docs_src/lifespan/tutorial002.py"
|
||||
```
|
||||
|
||||
`database` lives at module level for one reason: so you can look at it from *outside* the server.
|
||||
|
||||
!!! check
|
||||
Three moments, three values:
|
||||
|
||||
* Before the server starts, `database.connected` is `False`. Importing the module connected nothing.
|
||||
* While it's running, call `database_status` and the result is `"connected"`.
|
||||
* Stop the server and the `finally` block runs: `database.connected` is `False` again.
|
||||
|
||||
The work happened exactly where you put it: around the `yield`, not at import time and not per request.
|
||||
|
||||
## Recap
|
||||
|
||||
* `lifespan=` takes an `@asynccontextmanager` that receives the server and `yield`s one object.
|
||||
* Code before the `yield` is startup. The `finally` after it is shutdown.
|
||||
* It runs once, around the whole life of the server, not per request.
|
||||
* Whatever you `yield` is `ctx.request_context.lifespan_context` in every tool, resource, and prompt.
|
||||
* `ctx: Context[AppContext]` makes that access fully typed in tools. Resources and prompts take the bare `Context`.
|
||||
* No `lifespan=` means an empty `dict`, never `None`.
|
||||
|
||||
Next: tools that return more than text, **Media**.
|
||||
78
docs/tutorial/logging.md
Normal file
78
docs/tutorial/logging.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Logging
|
||||
|
||||
Log from a tool the way you log from any other Python function: with the standard library.
|
||||
|
||||
MCP has a protocol-level **logging capability**: a server could push its log messages to the client as notifications, through methods on the `Context` object. The 2026-07-28 revision of the spec **deprecates that capability and does not replace it**, so this tutorial doesn't teach it. The full list of what's deprecated and what to do instead is in **Deprecated features**.
|
||||
|
||||
What you do instead is what you do in every other Python program: the standard library.
|
||||
|
||||
## A tool that logs
|
||||
|
||||
```python title="server.py" hl_lines="1 5 13"
|
||||
--8<-- "docs_src/logging/tutorial001.py"
|
||||
```
|
||||
|
||||
* `logging.getLogger(__name__)` gives you a logger named after your module. Create it once, at the top.
|
||||
* Inside the tool you call `logger.info(...)` like in any other function. Nothing to inject, nothing to `await`, nothing MCP-specific.
|
||||
|
||||
!!! check
|
||||
Call the tool and look at the whole result:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(text="Found 3 books matching 'dune'.")]
|
||||
result.structured_content # {'result': "Found 3 books matching 'dune'."}
|
||||
```
|
||||
|
||||
The log line is nowhere in it. Logging is for **you**, the person operating the server. The model
|
||||
never sees it. If the model should read something, `return` it.
|
||||
|
||||
## Where it goes
|
||||
|
||||
For a **stdio** server this question matters more than usual. The host launched your server as a subprocess and is reading MCP messages from its **stdout**. Standard error is yours.
|
||||
|
||||
The standard library already does the right thing: log output goes to `sys.stderr` by default. Your `logger.info(...)` lines land in the terminal (or wherever the host collects the subprocess's stderr), and the protocol stream stays clean.
|
||||
|
||||
!!! tip
|
||||
Never `print()` in a stdio server. `print` writes to **stdout**, and stdout *is* the wire: one stray
|
||||
line and the client is trying to parse it as JSON-RPC.
|
||||
|
||||
`logger.debug("got here")` is the same one line of effort and goes to the right place.
|
||||
|
||||
## The level
|
||||
|
||||
You don't have to call `logging.basicConfig()` yourself. Constructing an `MCPServer` already did, with a handler pointed at standard error, at the level you pass as `log_level=`, so `MCPServer("Bookshop", log_level="DEBUG")` is all it takes to see your `logger.debug(...)` lines.
|
||||
|
||||
The default is `"INFO"`.
|
||||
|
||||
`logging.basicConfig()` never replaces handlers that already exist. If you configure logging yourself before creating the server, your configuration wins.
|
||||
|
||||
## Try it
|
||||
|
||||
Run the server with the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Call `search_books` from the **Tools** tab. The Inspector shows you the result: only the return value. The line
|
||||
|
||||
```text
|
||||
Searching for 'dune'
|
||||
```
|
||||
|
||||
went to standard error: the terminal, not the wire.
|
||||
|
||||
!!! info
|
||||
If what you actually want is *tracing* (every request, how long it took, whether it failed), you
|
||||
don't want log lines, you want spans. The SDK ships an `OpenTelemetryMiddleware` for exactly that.
|
||||
See **Middleware**.
|
||||
|
||||
## Recap
|
||||
|
||||
* The MCP protocol's logging capability is deprecated by the 2026-07-28 spec and not replaced. Don't build on it.
|
||||
* `logger = logging.getLogger(__name__)` at module level, `logger.info(...)` in the tool. That's the whole pattern.
|
||||
* Log output never reaches the model. Only the value you `return` does.
|
||||
* Standard error is yours; stdout belongs to the protocol. Never `print()` in a stdio server.
|
||||
* `MCPServer(..., log_level="DEBUG")` sets the level, and a logging configuration you made first is left alone.
|
||||
|
||||
Next: the in-memory client that has been running every example on these pages, and how to point it at your own server, in **Testing**.
|
||||
108
docs/tutorial/media.md
Normal file
108
docs/tutorial/media.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Media
|
||||
|
||||
Text is not the only thing a tool can return.
|
||||
|
||||
The SDK ships two helpers for binary results (**`Image`** and **`Audio`**) and an **`Icon`** type for giving your server, tools, resources, and prompts a face in the client's UI.
|
||||
|
||||
## Returning an image
|
||||
|
||||
Annotate the return type as `Image` and return one:
|
||||
|
||||
```python title="server.py" hl_lines="14 16"
|
||||
--8<-- "docs_src/media/tutorial001.py"
|
||||
```
|
||||
|
||||
* `Image` takes exactly one of `data` (raw bytes) or `path` (a file to read).
|
||||
* `format="png"` becomes the MIME type the client sees: `image/png`.
|
||||
* The bytes here are a one-pixel placeholder so the file runs on its own. In a real server they come from Pillow, matplotlib, a headless browser, or anything else that hands you `bytes`.
|
||||
|
||||
`Image` is an SDK convenience, not a protocol type. On the wire your return value becomes an **`ImageContent`** block (your bytes base64-encoded, plus the MIME type):
|
||||
|
||||
```python
|
||||
result.content # [ImageContent(type="image", data="iVBORw0KGgoAAAANSUhEUg...", mime_type="image/png")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Two things to notice:
|
||||
|
||||
* `data` is base64. You returned raw `bytes`; the SDK did the encoding.
|
||||
* `structured_content` is `None`. An `Image` is content for the model to look at, not data for the application to parse: there is no output schema. (Contrast **Structured Output**, where the return annotation *is* the schema.)
|
||||
|
||||
!!! info
|
||||
`ImageContent` and `AudioContent` live in `mcp_types`, right next to the `TextContent`
|
||||
you met in **Tools**. A tool result is a list of content blocks; `Image` and `Audio` are
|
||||
the shortest way to produce the two binary kinds.
|
||||
|
||||
### Try it
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Open the **Tools** tab and call `logo`. The result is not a string: it is an `image` content block, and the Inspector renders it as a picture. You returned `bytes`; everything between that and the pixels on screen was the SDK.
|
||||
|
||||
## Returning audio
|
||||
|
||||
`Audio` is the same shape:
|
||||
|
||||
```python title="server.py" hl_lines="21-24"
|
||||
--8<-- "docs_src/media/tutorial002.py"
|
||||
```
|
||||
|
||||
The result is an **`AudioContent`** block:
|
||||
|
||||
```python
|
||||
result.content # [AudioContent(type="audio", data="UklGRjQAAABXQVZFZm1...", mime_type="audio/wav")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Same deal: raw bytes in, base64 and a MIME type out, no output schema.
|
||||
|
||||
## Bytes or a file
|
||||
|
||||
Both helpers also accept `path=` instead of `data=`. The file is read when the result is built, and the MIME type is guessed from the suffix:
|
||||
|
||||
* `Image`: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`.
|
||||
* `Audio`: `.wav`, `.mp3`, `.ogg`, `.flac`, `.aac`, `.m4a`.
|
||||
|
||||
A suffix it doesn't recognise falls back to `application/octet-stream`.
|
||||
|
||||
!!! check
|
||||
With `data=` there is no filename, so there is nothing to guess from. Forget `format=` and
|
||||
the SDK falls back to a default: `image/png` for images, `audio/wav` for audio. Build an
|
||||
`Audio` from MP3 bytes that way and the client is told `mime_type="audio/wav"`, then
|
||||
faithfully fails to decode it. When you pass `data=`, pass `format=`.
|
||||
|
||||
## Icons
|
||||
|
||||
An `Icon` is metadata, not content. It doesn't carry the image; it points at one with a URI, and a client may fetch it and show it next to your server's name, a tool, a resource, or a prompt.
|
||||
|
||||
```python title="server.py" hl_lines="5-6 8 11 17"
|
||||
--8<-- "docs_src/media/tutorial003.py"
|
||||
```
|
||||
|
||||
* `src` is a URI the client can resolve: `https:`, or a `data:` URI if you want the icon embedded with no extra fetch.
|
||||
* `mime_type` and `sizes` (`"48x48"`, or `"any"` for a scalable format) let the client pick the right one when you offer several.
|
||||
* `theme="light"` or `theme="dark"` marks an icon for one colour scheme.
|
||||
|
||||
The same `icons=[...]` keyword is accepted by `MCPServer(...)`, `@mcp.tool()`, `@mcp.resource()`, and `@mcp.prompt()`.
|
||||
|
||||
### Where a client sees them
|
||||
|
||||
Icons travel with whatever they decorate. The server's arrive during the handshake, on `client.server_info`:
|
||||
|
||||
```python
|
||||
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
|
||||
```
|
||||
|
||||
A tool's icons are on the `Tool` object from `tools/list`, a resource's on the `Resource` from `resources/list`, a prompt's on the `Prompt` from `prompts/list`. The field is always called `icons`.
|
||||
|
||||
## Recap
|
||||
|
||||
* Return an `Image` or `Audio` from a tool and the client receives an `ImageContent` / `AudioContent` block: your bytes base64-encoded, with a MIME type.
|
||||
* Build one from in-memory `data=` plus an explicit `format=`, or from a `path=` and let the suffix decide.
|
||||
* Media results carry no `structured_content` and no output schema.
|
||||
* An `Icon` is a pointer: a `src` URI plus optional `mime_type`, `sizes`, and `theme`.
|
||||
* `icons=[...]` works on the server, on tools, on resources, and on prompts, and clients find them on the matching objects.
|
||||
|
||||
That is everything a tool can put *into* a result. Helping the user fill in a prompt's or a resource template's arguments *before* anything runs is **Completions**.
|
||||
117
docs/tutorial/progress.md
Normal file
117
docs/tutorial/progress.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Progress
|
||||
|
||||
A tool that takes thirty seconds and says nothing for thirty seconds looks broken.
|
||||
|
||||
**Progress notifications** fix that. The tool reports how far along it is; the client decides what to draw with it: a bar, a spinner, a log line.
|
||||
|
||||
## Report it from the tool
|
||||
|
||||
Take a **`Context`** parameter and call `report_progress`:
|
||||
|
||||
```python title="server.py" hl_lines="8 11"
|
||||
--8<-- "docs_src/progress/tutorial001.py"
|
||||
```
|
||||
|
||||
Three arguments, and you decide what they mean:
|
||||
|
||||
* `progress`: how far you are. The spec requires it to **increase** with every report; never repeat a value or go backwards.
|
||||
* `total`: how much there is in total, if you know. Optional.
|
||||
* `message`: one human-readable line about *this* step. Optional.
|
||||
|
||||
`ctx` is injected because of its type hint and the model never sees it: `import_catalog`'s input schema has a single property, `urls`. **The Context** chapter is all about that object; progress is one of the things it gives you.
|
||||
|
||||
## Listen for it from the client
|
||||
|
||||
The client opts in **per call**, by passing `progress_callback=` to `call_tool`:
|
||||
|
||||
```python title="client.py" hl_lines="7 16"
|
||||
import anyio
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def show(progress: float, total: float | None, message: str | None) -> None:
|
||||
print(f"{message} ({progress}/{total})")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool(
|
||||
"import_catalog",
|
||||
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
|
||||
progress_callback=show,
|
||||
)
|
||||
print(result.structured_content)
|
||||
|
||||
|
||||
anyio.run(main)
|
||||
```
|
||||
|
||||
The callback is an `async` function taking exactly what the server reported: `progress`, `total`, `message`.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` connects straight to the server object, in memory, the same client the **Testing**
|
||||
chapter is built on. `progress_callback` is the same parameter whatever transport the `Client`
|
||||
uses; the *timing* you are about to see is the in-memory connection's. It runs your callback
|
||||
inline, so every report lands before `call_tool` returns. Over a real transport the
|
||||
notifications race the result, and a slow callback can still be running after `call_tool` has
|
||||
returned.
|
||||
|
||||
### Try it
|
||||
|
||||
Put `client.py` next to `server.py` and run it:
|
||||
|
||||
```console
|
||||
python client.py
|
||||
```
|
||||
|
||||
```text
|
||||
Imported https://example.com/a.json (1/2)
|
||||
Imported https://example.com/b.json (2/2)
|
||||
{'result': 'Imported 2 records.'}
|
||||
```
|
||||
|
||||
Every `await ctx.report_progress(...)` on the server became one call to `show` on the client, in order, and both lines printed **before** `call_tool` returned. Progress is not bundled into the result; it streams while the tool is still working.
|
||||
|
||||
!!! warning
|
||||
`progress_callback` belongs to the **call**, not the `Client`. There is no constructor argument
|
||||
for it, because different calls want different callbacks: one drives a download bar, the next
|
||||
one a log line.
|
||||
|
||||
!!! check
|
||||
Now delete `progress_callback=show` and run it again:
|
||||
|
||||
```text
|
||||
{'result': 'Imported 2 records.'}
|
||||
```
|
||||
|
||||
No error, no warning, same result. `report_progress` is a **no-op when the caller didn't ask
|
||||
for progress**, so you report unconditionally and never have to wonder whether anyone is
|
||||
listening.
|
||||
|
||||
## When you don't know the total
|
||||
|
||||
`total` is for when you know the denominator. Often you don't: you're draining a feed, walking a cursor, downloading something with no length header.
|
||||
|
||||
Leave it out:
|
||||
|
||||
```python title="server.py" hl_lines="20"
|
||||
--8<-- "docs_src/progress/tutorial002.py"
|
||||
```
|
||||
|
||||
The callback receives `total=None`. A client can still show *activity* ("3 imported so far...") but it can't show a percentage. Don't invent a total to get a prettier bar.
|
||||
|
||||
!!! tip
|
||||
`progress` doesn't have to count anything in particular. Bytes, rows, pages: pick the unit the
|
||||
user would recognise, and only promise a `total` you can keep.
|
||||
|
||||
## Recap
|
||||
|
||||
* `await ctx.report_progress(progress, total=None, message=None)` from any tool that takes a `Context`.
|
||||
* The client passes `progress_callback=` to `call_tool`: per call, never on the `Client`.
|
||||
* The callback is `async (progress, total, message) -> None` and fires while the tool is still running.
|
||||
* No callback on the call means `report_progress` does nothing. Report unconditionally.
|
||||
* Omit `total` when you don't know it; the callback gets `None`.
|
||||
|
||||
Progress is what a running tool shows the *user*. The lines it logs for *you*, the person operating the server, are a different channel: **Logging** is next.
|
||||
150
docs/tutorial/prompts.md
Normal file
150
docs/tutorial/prompts.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# Prompts
|
||||
|
||||
A **prompt** is a message template the user picks.
|
||||
|
||||
Tools are for the model. A prompt is the opposite: the user chooses one from a menu in their client (a slash command, a button), fills in its arguments, and the rendered messages go into the conversation as if they had typed them.
|
||||
|
||||
You declare one by putting `@mcp.prompt()` on a function that returns the text.
|
||||
|
||||
## Your first prompt
|
||||
|
||||
```python title="server.py" hl_lines="6-9"
|
||||
--8<-- "docs_src/prompts/tutorial001.py"
|
||||
```
|
||||
|
||||
The SDK reads the same three things it read from your tools:
|
||||
|
||||
* The **name** is the function name: `review_code`.
|
||||
* The **description** the client shows is the docstring: `Review a piece of code.`
|
||||
* The **arguments** come from the parameters. `code` has no default, so it's required.
|
||||
|
||||
That is what a client gets back from `prompts/list`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "review_code",
|
||||
"description": "Review a piece of code.",
|
||||
"arguments": [
|
||||
{"name": "code", "required": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
There is no JSON Schema here. Prompt arguments are a flat list of **named string values**: a form a person fills in, not a payload a model constructs.
|
||||
|
||||
### Rendering it
|
||||
|
||||
The client renders the template with `prompts/get`, passing the arguments. Your function runs and the `str` you return becomes **one user message**:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Review a piece of code.",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "Please review this code:\n\ndef add(a, b): return a + b"
|
||||
}
|
||||
}
|
||||
],
|
||||
"resultType": "complete"
|
||||
}
|
||||
```
|
||||
|
||||
That is the entire life of a prompt: listed by name, rendered on demand, dropped into the chat.
|
||||
|
||||
!!! check
|
||||
`required` is enforced before your function runs. Render `review_code` without `code` and the
|
||||
request itself fails with a JSON-RPC error (code `-32603`):
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: Internal server error
|
||||
```
|
||||
|
||||
There is no tool-style error result to hand back to a model, because no model is in the loop:
|
||||
the call raises. The reason (`Missing required arguments: {'code'}`) lands in your server's log.
|
||||
|
||||
### Try it
|
||||
|
||||
Run the server with the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Open the **Prompts** tab and select `review_code`. The Inspector draws a form with one required `code` field. Fill it in, render it, and you get back exactly the user message above.
|
||||
|
||||
## More than one message
|
||||
|
||||
A code review is one message. A debugging session is a conversation, and a prompt can seed the whole thing.
|
||||
|
||||
Return a list of messages instead of a `str`:
|
||||
|
||||
```python title="server.py" hl_lines="2 13-20"
|
||||
--8<-- "docs_src/prompts/tutorial002.py"
|
||||
```
|
||||
|
||||
* `UserMessage` and `AssistantMessage` come from `mcp.server.mcpserver.prompts.base`. Hand them a `str` and they wrap it in `TextContent` for you. The role is the class name.
|
||||
* `Message` is their common base. Use it as the return annotation.
|
||||
|
||||
Rendering `debug_error` now produces three messages, in order:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Start a debugging conversation.",
|
||||
"messages": [
|
||||
{"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}},
|
||||
{"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"}
|
||||
}
|
||||
],
|
||||
"resultType": "complete"
|
||||
}
|
||||
```
|
||||
|
||||
Notice the last one. Pre-filling an `assistant` turn is how you steer the model's *next* reply without making the user type the steering themselves.
|
||||
|
||||
## Titles and argument descriptions
|
||||
|
||||
`review_code` is a function name, not a label. Give the client something better to put on the button, and describe each argument so the form explains itself:
|
||||
|
||||
```python title="server.py" hl_lines="10-13"
|
||||
--8<-- "docs_src/prompts/tutorial003.py"
|
||||
```
|
||||
|
||||
* `title="Code review"` is the human-readable name, exactly like a tool's `title`.
|
||||
* `Annotated[str, Field(description=...)]` is the same pattern you used in **Tools**. Here the description lands on the argument instead of in a schema.
|
||||
* `language` has a default, so it stops being required.
|
||||
|
||||
The `prompts/list` entry now carries everything a client needs to draw a good form:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "review_code",
|
||||
"title": "Code review",
|
||||
"description": "Review a piece of code.",
|
||||
"arguments": [
|
||||
{"name": "code", "description": "The code to review.", "required": true},
|
||||
{"name": "language", "description": "The language the code is written in.", "required": false}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
!!! info
|
||||
If you have read **Tools**, you already know everything on this page. Same decorator, same
|
||||
docstring-as-description, same `Annotated`/`Field`. The only things that change are who
|
||||
triggers it (the user) and where the result goes (into the conversation).
|
||||
|
||||
## Recap
|
||||
|
||||
* `@mcp.prompt()` on a function makes it a prompt. Name from the function, description from the docstring.
|
||||
* Prompts are **user-controlled**: the client lists them, the user picks one and fills in the arguments.
|
||||
* Arguments are a flat list of named strings (no schema). A parameter with a default is optional.
|
||||
* Return a `str` and it becomes one user message. Return a list of `UserMessage` / `AssistantMessage` to seed a multi-turn conversation.
|
||||
* `title=` and `Field(description=...)` are what a client puts in its UI.
|
||||
* A missing required argument fails the whole request. There is no per-prompt error result.
|
||||
|
||||
Next up: the one extra parameter a tool, resource or prompt can ask the SDK for, **The Context**.
|
||||
139
docs/tutorial/resources.md
Normal file
139
docs/tutorial/resources.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Resources
|
||||
|
||||
A **resource** is data you expose for the application to read.
|
||||
|
||||
That's the split. A tool is something the **model** decides to call. A resource is something the **application** decides to load (a config file, a record, a document) and put in front of the model as context.
|
||||
|
||||
You declare one by putting `@mcp.resource(uri)` on a plain Python function.
|
||||
|
||||
## Your first resource
|
||||
|
||||
```python title="server.py" hl_lines="6-8"
|
||||
--8<-- "docs_src/resources/tutorial001.py"
|
||||
```
|
||||
|
||||
It's the same shape as a tool, plus one thing: the **URI**. Resources are addressed, not named. A client asks for `config://app`, never for `get_config`.
|
||||
|
||||
The SDK still reads the rest from the function:
|
||||
|
||||
* The **name** is the function name: `get_config`.
|
||||
* The **description** the client sees is the docstring.
|
||||
* The **content** is whatever you return.
|
||||
|
||||
During `resources/list` the client gets this:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "get_config",
|
||||
"uri": "config://app",
|
||||
"description": "The active shop configuration.",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
```
|
||||
|
||||
And when it reads `config://app`, your function runs and the return value comes back as text:
|
||||
|
||||
```python
|
||||
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Listing is cheap. Your function is **not** called during `resources/list`, only during
|
||||
`resources/read`, and only for the URI that was asked for. Expose a thousand resources
|
||||
and you pay for the ones somebody opens.
|
||||
|
||||
### Try it
|
||||
|
||||
Run the server with the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Open the URL it prints and go to the **Resources** tab. `config://app` is in the list with its description. Click it and the Inspector reads it: there are your two lines of config.
|
||||
|
||||
## Resource templates
|
||||
|
||||
One URI per record doesn't scale. Put a **placeholder** in the URI and a matching parameter on the function:
|
||||
|
||||
```python title="server.py" hl_lines="12-13"
|
||||
--8<-- "docs_src/resources/tutorial002.py"
|
||||
```
|
||||
|
||||
`{user_id}` in the URI, `user_id: str` on the function. That is the entire contract.
|
||||
|
||||
This is now a **resource template**, and it moves house: it leaves `resources/list` and shows up in `resources/templates/list` instead, as a pattern rather than an address:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "get_user_profile",
|
||||
"uriTemplate": "users://{user_id}/profile",
|
||||
"description": "A customer's profile.",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
```
|
||||
|
||||
The client fills in the placeholder and reads a concrete URI: `users://42/profile`, `users://ada/profile`. One function answers all of them, with the matched value passed in as `user_id`:
|
||||
|
||||
```python
|
||||
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
|
||||
```
|
||||
|
||||
Notice the `uri` in the result. It is the **concrete** URI the client asked for, not the template.
|
||||
|
||||
!!! check
|
||||
The placeholders and the parameters have to agree. Rename the function parameter to
|
||||
`user` while the URI still says `{user_id}` and the decorator refuses **at import time**,
|
||||
before any client gets near it:
|
||||
|
||||
```text
|
||||
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
|
||||
```
|
||||
|
||||
A mismatch can only ever be a bug, so the SDK makes it impossible to start the server with one.
|
||||
|
||||
`get_user_profile` can also take a parameter annotated `Context`. The SDK injects it without ever treating it as a URI parameter, and **The Context** chapter covers what it gives you.
|
||||
|
||||
## What you return
|
||||
|
||||
You're not limited to `str`. Give each resource a `mime_type` and return whatever fits:
|
||||
|
||||
```python title="server.py" hl_lines="8-9 14-15 20-21"
|
||||
--8<-- "docs_src/resources/tutorial003.py"
|
||||
```
|
||||
|
||||
* `readme` returns a `str`, so it's sent as-is. This is the common case.
|
||||
* `catalog_stats` returns a `dict`, so the SDK serialises it to **JSON text** for you:
|
||||
|
||||
```json
|
||||
{
|
||||
"books": 1204,
|
||||
"authors": 391
|
||||
}
|
||||
```
|
||||
|
||||
* `placeholder_cover` returns `bytes`, so the client gets a `BlobResourceContents` instead of a `TextResourceContents`, with your bytes base64-encoded in its `blob` field.
|
||||
|
||||
The same rule applies to anything else JSON-serialisable: a list, a Pydantic model, a dataclass. If it isn't a `str` and isn't `bytes`, it becomes JSON.
|
||||
|
||||
`mime_type` is yours to declare, and it defaults to `text/plain`. The SDK never inspects what you return to guess it, so a `dict` resource you don't label is still advertised as plain text.
|
||||
|
||||
!!! tip
|
||||
`name=`, `title=` and `description=` are also accepted by `@mcp.resource()` when you don't
|
||||
want to derive them from the function. And when there's no function to write at all,
|
||||
`mcp.server.mcpserver.resources` has ready-made `Resource` classes (`TextResource`,
|
||||
`BinaryResource`, `FileResource`, `HttpResource`, `DirectoryResource`) that you register
|
||||
with `mcp.add_resource(...)`.
|
||||
|
||||
A client can also **subscribe** to a resource and be notified when it changes; that's the client's half of the story and it lives in **The Client**.
|
||||
|
||||
## Recap
|
||||
|
||||
* `@mcp.resource(uri)` on a function makes it a resource. The URI is the address, the return value is the content, the docstring is the description.
|
||||
* A `{placeholder}` in the URI makes it a **template**: it's listed under `resources/templates/list` and one function serves every URI that matches.
|
||||
* Placeholder names must equal the function's parameter names. Get it wrong and you find out at import time, not in production.
|
||||
* Your function runs when the resource is **read**, not when it's listed.
|
||||
* `str` becomes text, `bytes` becomes a base64 blob, anything else becomes JSON text. `mime_type=` is how you label it.
|
||||
* Tools are for the model to act. Resources are for the application to read.
|
||||
|
||||
Next: the third primitive, the one a person picks from a menu, **Prompts**.
|
||||
245
docs/tutorial/structured-output.md
Normal file
245
docs/tutorial/structured-output.md
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# Structured Output
|
||||
|
||||
In **Tools** you returned a `str` and the result came back twice: as text in `content`, and as `{"result": "..."}` in `structured_content`.
|
||||
|
||||
This chapter is about that second channel: where it comes from, every shape it can take, and how the SDK keeps it honest.
|
||||
|
||||
The short version: **the return type annotation is the output schema**. You already wrote it.
|
||||
|
||||
## The output schema
|
||||
|
||||
```python title="server.py" hl_lines="9"
|
||||
--8<-- "docs_src/structured_output/tutorial001.py"
|
||||
```
|
||||
|
||||
The line that matters is the signature: `-> int`.
|
||||
|
||||
Because of it, the tool the SDK sends during `tools/list` carries an `output_schema` next to the input schema you met in **Tools**:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"result": {"title": "Result", "type": "integer"}
|
||||
},
|
||||
"required": ["result"],
|
||||
"title": "get_temperatureOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
A bare `int` isn't a JSON object, so the SDK **wraps** it in `{"result": ...}`. Call the tool and both channels are filled:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(text="17")]
|
||||
result.structured_content # {"result": 17}
|
||||
```
|
||||
|
||||
Every scalar gets the same wrapper: `str`, `int`, `float`, `bool`, `bytes`, `None`.
|
||||
|
||||
## Two channels
|
||||
|
||||
Why send the same value twice?
|
||||
|
||||
* `content` is for the **model**. A language model reads text; this is the only part of the result it sees.
|
||||
* `structured_content` is for the **application** the model runs inside: code that wants `17`, not a sentence containing "17".
|
||||
* `output_schema` is the contract between them, published before the tool is ever called.
|
||||
|
||||
You return one Python value. The SDK fills in all three.
|
||||
|
||||
## Return a model
|
||||
|
||||
Declare the shape as a Pydantic `BaseModel` and return an instance:
|
||||
|
||||
```python title="server.py" hl_lines="8-11 15"
|
||||
--8<-- "docs_src/structured_output/tutorial002.py"
|
||||
```
|
||||
|
||||
`WeatherData` **is** the schema now. No wrapper, no `result` key:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
|
||||
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
|
||||
"conditions": {"title": "Conditions", "type": "string"}
|
||||
},
|
||||
"required": ["temperature", "humidity", "conditions"],
|
||||
"title": "WeatherData",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
`structured_content` is the object, field for field:
|
||||
|
||||
```python
|
||||
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
|
||||
```
|
||||
|
||||
And the model is not left out. The SDK serializes the same object to JSON text for `content`:
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 16.2,
|
||||
"humidity": 0.83,
|
||||
"conditions": "Overcast"
|
||||
}
|
||||
```
|
||||
|
||||
Notice the `Field(description=...)` on `temperature` and `humidity` landed in the schema. The same `Field` that described your **inputs** describes your outputs.
|
||||
|
||||
!!! info
|
||||
If you've used FastAPI's `response_model`, you already know this: a Pydantic model as the declared
|
||||
response, serialized and documented for you. The only difference is that here the return annotation
|
||||
is the whole declaration.
|
||||
|
||||
## A `TypedDict`
|
||||
|
||||
Not every shape deserves a class. A `TypedDict` produces the same schema:
|
||||
|
||||
```python title="server.py" hl_lines="8"
|
||||
--8<-- "docs_src/structured_output/tutorial003.py"
|
||||
```
|
||||
|
||||
A `TypedDict` is a plain `dict` at runtime, so that is what you build and return. The schema, the validation, and `structured_content` are identical to the `BaseModel` version (minus the descriptions, which `TypedDict` has no place for).
|
||||
|
||||
## A dataclass
|
||||
|
||||
Dataclasses work too, and so does any ordinary class whose attributes have type hints. The SDK builds a Pydantic model out of the annotations behind the scenes.
|
||||
|
||||
```python title="server.py" hl_lines="8-9"
|
||||
--8<-- "docs_src/structured_output/tutorial004.py"
|
||||
```
|
||||
|
||||
Three spellings, one schema. Use whichever your codebase already has.
|
||||
|
||||
## Lists
|
||||
|
||||
A `list[...]` isn't a JSON object either, so it gets the `{"result": ...}` wrapper, with your item type as a `$defs` reference inside it:
|
||||
|
||||
```python title="server.py" hl_lines="15"
|
||||
--8<-- "docs_src/structured_output/tutorial005.py"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"$defs": {
|
||||
"WeatherData": {
|
||||
"properties": {
|
||||
"temperature": {"title": "Temperature", "type": "number"},
|
||||
"humidity": {"title": "Humidity", "type": "number"},
|
||||
"conditions": {"title": "Conditions", "type": "string"}
|
||||
},
|
||||
"required": ["temperature", "humidity", "conditions"],
|
||||
"title": "WeatherData",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
|
||||
},
|
||||
"required": ["result"],
|
||||
"title": "get_forecastOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
Ask for a two-day forecast and `structured_content` is `{"result": [{...}, {...}]}`. `content` becomes **two** `TextContent` blocks, one per item: a list is flattened for the model rather than dumped as one string.
|
||||
|
||||
`tuple[...]`, unions, and `Optional[...]` are wrapped the same way.
|
||||
|
||||
## Dictionaries
|
||||
|
||||
`dict[str, ...]` is the one generic that already *is* a JSON object, so it isn't wrapped:
|
||||
|
||||
```python title="server.py" hl_lines="9"
|
||||
--8<-- "docs_src/structured_output/tutorial006.py"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"additionalProperties": {"type": "number"},
|
||||
"title": "get_temperaturesDictOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
|
||||
```
|
||||
|
||||
The keys must be `str`. A `dict[int, float]` can't be a JSON object, so it falls back to the `{"result": ...}` wrapper.
|
||||
|
||||
## Validation
|
||||
|
||||
`output_schema` is not documentation. Whatever your function returns is **validated against it** before it leaves the server.
|
||||
|
||||
You don't notice while you build the value by hand: Pydantic already made sure your `WeatherData` was a `WeatherData`. You notice the day the data comes from somewhere you don't control:
|
||||
|
||||
```python title="server.py" hl_lines="9 21"
|
||||
--8<-- "docs_src/structured_output/tutorial007.py"
|
||||
```
|
||||
|
||||
The annotation promises `WeatherData`. The upstream response stopped sending `humidity`.
|
||||
|
||||
!!! check
|
||||
Call `get_weather` and it does not quietly hand the client a half-empty object. The call fails,
|
||||
and the first lines of the error name the field:
|
||||
|
||||
```text
|
||||
Error executing tool get_weather: 1 validation error for WeatherData
|
||||
humidity
|
||||
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
|
||||
```
|
||||
|
||||
That text comes back as the tool result with `is_error=True`, so the model knows the call failed
|
||||
instead of confidently reading weather that isn't there.
|
||||
|
||||
Returning a plain `dict` from a `-> WeatherData` tool is fine, by the way. That's exactly what `json.loads` produced. Validation is on the value, not on the Python type.
|
||||
|
||||
## Opting out
|
||||
|
||||
Sometimes the return annotation is for your type checker, not for the protocol. Pass `structured_output=False` and the tool is text-only:
|
||||
|
||||
```python title="server.py" hl_lines="6"
|
||||
--8<-- "docs_src/structured_output/tutorial008.py"
|
||||
```
|
||||
|
||||
No `output_schema`, no wrapping, no validation. `structured_content` is `None` and `content` is the string you returned.
|
||||
|
||||
The opposite, `structured_output=True`, turns the automatic detection into a requirement: a tool whose return type can't produce a schema raises at import time instead of falling back to text.
|
||||
|
||||
## A class without type hints
|
||||
|
||||
There is one way to end up unstructured without asking for it: return a class that has **no annotations on its body**.
|
||||
|
||||
```python title="server.py" hl_lines="6-9"
|
||||
--8<-- "docs_src/structured_output/tutorial009.py"
|
||||
```
|
||||
|
||||
`Station` sets `name` and `online` inside `__init__`, but the *class* declares nothing. The SDK reads class annotations, finds none, and gives up.
|
||||
|
||||
!!! warning
|
||||
It gives up **silently**. `output_schema` is `None`, `structured_content` is `None`, and the text
|
||||
the model reads is the object's `repr`:
|
||||
|
||||
```text
|
||||
"<server.Station object at 0x7f539d75b230>"
|
||||
```
|
||||
|
||||
No error, no warning, a useless tool. Move the annotations onto the class body, or pass
|
||||
`structured_output=True`, which turns this into a hard error the moment the module imports:
|
||||
`Function get_station: return type <class 'server.Station'> is not serializable for structured output`.
|
||||
|
||||
!!! tip
|
||||
Need full control (building the `CallToolResult` yourself, or attaching `_meta` that the
|
||||
application can see but the model can't)? That's **The low-level Server**.
|
||||
|
||||
## Recap
|
||||
|
||||
* The **return type annotation** is the output schema. It's published in `tools/list` as `output_schema`.
|
||||
* Scalars, lists, tuples and unions are wrapped in `{"result": ...}`. Models, `TypedDict`s, dataclasses, annotated classes and `dict[str, ...]` are objects already and stay as they are.
|
||||
* Every result carries `content` (text, for the model) **and** `structured_content` (data, for the application).
|
||||
* What you return is validated against the schema. A mismatch is a tool error, not a corrupt result.
|
||||
* `structured_output=False` opts a tool out. A class without type hints opts out silently; watch for it.
|
||||
|
||||
You now own everything a tool can say back. Next, the second primitive: **Resources**.
|
||||
106
docs/tutorial/testing.md
Normal file
106
docs/tutorial/testing.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Testing
|
||||
|
||||
The Python SDK ships a `Client` class with an **in-memory transport**: pass it your server object and it connects to it directly.
|
||||
|
||||
No subprocess. No port. No transport at all. It's the same idea as FastAPI's `TestClient`.
|
||||
|
||||
## Basic usage
|
||||
|
||||
Let's assume you have a simple server with a single tool:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/testing/tutorial001.py"
|
||||
```
|
||||
|
||||
To run the test below you'll need two extra (development) dependencies:
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add --dev pytest inline-snapshot
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install pytest inline-snapshot
|
||||
```
|
||||
|
||||
!!! info
|
||||
These docs assume you already know [`pytest`](https://docs.pytest.org/en/stable/).
|
||||
|
||||
[`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) is what the test below
|
||||
uses to assert on the whole result object in one line. It records the output of a test as the
|
||||
`snapshot(...)` literal you see. If you'd rather not use it, drop the import and assert on the
|
||||
fields you care about (`result.content[0].text == "3"`) like in any other test.
|
||||
|
||||
Now the test:
|
||||
|
||||
```python title="test_server.py"
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp import Client
|
||||
from mcp_types import CallToolResult, TextContent
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend(): # (1)!
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(): # (2)!
|
||||
async with Client(mcp, raise_exceptions=True) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_add_tool(client: Client):
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result == snapshot(
|
||||
CallToolResult(
|
||||
content=[TextContent(type="text", text="3")],
|
||||
structured_content={"result": 3},
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
1. If you are using `trio`, return `"trio"` instead. See the [anyio documentation](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on) for the details.
|
||||
2. The fixture yields a connected client. Every test that takes `client` gets a fresh in-memory connection to the same server.
|
||||
|
||||
There you go! You can now extend your tests to cover more scenarios.
|
||||
|
||||
## Why `raise_exceptions=True`?
|
||||
|
||||
Two different things can go wrong, and this flag only touches one of them.
|
||||
|
||||
An exception inside one of **your tools** is not a protocol failure. It becomes a normal result with
|
||||
`is_error=True`, and the model reads the message. `raise_exceptions` doesn't change that: with or
|
||||
without it, `call_tool` returns the same `is_error=True` result. There's a whole chapter on it:
|
||||
**Handling errors**.
|
||||
|
||||
A failure **outside** a tool body is different. On the connection `Client(mcp)` gives you, the
|
||||
server sanitises it into a generic `"Internal server error"` before the client sees it. You should
|
||||
never leak the details of an unexpected crash to a remote caller. In a test that is exactly what
|
||||
you *don't* want, and it is what `raise_exceptions=True` changes: your test sees the real message
|
||||
instead of the sanitised one.
|
||||
|
||||
Leave it on in tests. It has no meaning in production code.
|
||||
|
||||
## In-process by default
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` connects in-process and is **era-neutral** by default: it probes the server and
|
||||
picks the appropriate protocol path. Pin `mode="legacy"` if your test exercises legacy-specific
|
||||
semantics (sampling or elicitation push, `message_handler`), and drop `raise_exceptions=True`
|
||||
there: a legacy connection never sanitises in the first place, and the flag re-raises the
|
||||
failure inside the server task instead of in your test.
|
||||
|
||||
That one line is also why the rest of this tutorial can promise you that its examples work: every
|
||||
example file is exercised by the SDK's own test suite through exactly this client. You're using the
|
||||
same tool the SDK uses on itself.
|
||||
|
||||
The tutorial ends here. Putting your tested server in front of a real client, over a real
|
||||
transport, is **Running your server**.
|
||||
172
docs/tutorial/tools.md
Normal file
172
docs/tutorial/tools.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# Tools
|
||||
|
||||
A **tool** is a function the model can call.
|
||||
|
||||
You declare one by putting `@mcp.tool()` on a plain Python function. That's the whole API.
|
||||
|
||||
## Your first tool
|
||||
|
||||
```python title="server.py" hl_lines="6-8"
|
||||
--8<-- "docs_src/tools/tutorial001.py"
|
||||
```
|
||||
|
||||
Look at what you wrote. There are no schemas, no JSON, no protocol, just a function. The SDK reads three things from it:
|
||||
|
||||
* The **name** of the tool is the name of the function: `search_books`.
|
||||
* The **description** the model sees is the docstring: `Search the catalog by title or author.`
|
||||
* The **arguments** the model is allowed to pass come from the type hints: `query: str` and `limit: int`.
|
||||
|
||||
### The input schema
|
||||
|
||||
From those type hints the SDK generates a JSON Schema and sends it to the client during `tools/list`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query", "limit"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
Both arguments are in `required` because neither has a default. You'll fix that in a moment. (The `title` keys are Pydantic artifacts; the properties, their types, and `required` are the contract.)
|
||||
|
||||
!!! tip
|
||||
Type hints aren't documentation here. They are **the contract**. If a client sends `"limit": "ten"`,
|
||||
the SDK rejects it before your function ever runs.
|
||||
|
||||
### What the model gets back
|
||||
|
||||
Call the tool with `{"query": "dune", "limit": 5}` and the result has two parts:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
|
||||
result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
|
||||
```
|
||||
|
||||
`content` is the text the **model** reads. `structured_content` is typed data for the **client application**. It's there because you declared the return type as `-> str`.
|
||||
|
||||
Don't worry about `structured_content` yet. Return real Python objects from your tools and the right thing happens; the **Structured Output** chapter is all about it.
|
||||
|
||||
### Try it
|
||||
|
||||
Run the server with the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Open the URL it prints, go to the **Tools** tab, and call `search_books`.
|
||||
|
||||
The Inspector renders a form with a required `query` text field and a required `limit` number field. It built that form from your type hints. So will every other MCP client.
|
||||
|
||||
## Optional arguments
|
||||
|
||||
Give a parameter a default value and it stops being required. That's it. It's just Python.
|
||||
|
||||
```python title="server.py" hl_lines="7"
|
||||
--8<-- "docs_src/tools/tutorial002.py"
|
||||
```
|
||||
|
||||
The schema follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"default": 10, "title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
`limit` left `required` and gained `"default": 10`. A client that omits it gets `10`, exactly as Python would.
|
||||
|
||||
## Richer schemas with `Field`
|
||||
|
||||
Type hints get you a long way, but sometimes you want to *describe* an argument, or constrain it.
|
||||
|
||||
Wrap the type in `Annotated` and add a Pydantic `Field`:
|
||||
|
||||
```python title="server.py" hl_lines="12-14"
|
||||
--8<-- "docs_src/tools/tutorial003.py"
|
||||
```
|
||||
|
||||
Three new things, all on the parameters:
|
||||
|
||||
* `Field(description=...)`: a per-argument description the model reads alongside the docstring.
|
||||
* `Field(ge=1, le=50)`: numeric bounds. They land in the schema as `"minimum": 1, "maximum": 50`.
|
||||
* `Literal["fiction", "non-fiction", "poetry"]`: an enum. The model can only pick one of those.
|
||||
|
||||
!!! check
|
||||
Constraints are not decoration. Call the tool with `limit=999` and the SDK answers with a
|
||||
tool error **before your function runs**:
|
||||
|
||||
```text
|
||||
Input should be less than or equal to 50
|
||||
```
|
||||
|
||||
That error goes back to the model as the tool result, and the model reads it and retries with
|
||||
a valid value. You wrote `le=50` once and got self-correcting agents for free.
|
||||
|
||||
!!! info
|
||||
If you've used FastAPI or Pydantic, you already know all of this. It's the same `Field`,
|
||||
the same `Annotated`, the same validation. There is nothing MCP-specific to learn here.
|
||||
|
||||
## A model as a parameter
|
||||
|
||||
When a tool takes more than a couple of arguments, group them into a Pydantic model:
|
||||
|
||||
```python title="server.py" hl_lines="8-11 15"
|
||||
--8<-- "docs_src/tools/tutorial004.py"
|
||||
```
|
||||
|
||||
The `Book` schema is nested inside the tool's input schema (as a `$defs` reference), the model fills it in as a JSON object, and your function receives a **real `Book` instance**, already validated, with `.title`, `.author` and `.year` attributes.
|
||||
|
||||
You can mix and match: plain parameters next to model parameters, nested models, lists of models. It's Pydantic all the way down.
|
||||
|
||||
## `async def`
|
||||
|
||||
If a tool does I/O (calls an API, reads a file, queries a database), declare it `async def` and `await` inside it. The SDK awaits it.
|
||||
|
||||
A plain `def` tool works too: the SDK runs it in a thread so it never blocks the server.
|
||||
|
||||
There is nothing else to configure.
|
||||
|
||||
## Names, titles, and annotations
|
||||
|
||||
Everything the SDK infers, you can override in the decorator:
|
||||
|
||||
```python title="server.py" hl_lines="8-11"
|
||||
--8<-- "docs_src/tools/tutorial005.py"
|
||||
```
|
||||
|
||||
* `title` is a human-readable name for UIs. Clients show *"Search the catalog"* instead of `search_books`.
|
||||
* `annotations` are behavioural **hints** for the client:
|
||||
* `read_only_hint=True`: this tool doesn't change anything.
|
||||
* `open_world_hint=False`: it works on a closed set of things (this catalog), not the open web.
|
||||
* The other two, `destructive_hint` and `idempotent_hint`, describe a tool that *writes*: may it
|
||||
delete something, and is calling it twice the same as calling it once? The spec defines both
|
||||
only for non-read-only tools, so they would say nothing on `search_books`.
|
||||
|
||||
A well-behaved client uses them to decide things like *"do I need to ask the user before running this?"*. They are hints, not security. Never rely on a client honouring them.
|
||||
|
||||
!!! tip
|
||||
`name=` and `description=` are also accepted by `@mcp.tool()` if you don't want to derive them
|
||||
from the function name and docstring. Most of the time you do.
|
||||
|
||||
## Recap
|
||||
|
||||
* `@mcp.tool()` on a function makes it a tool. Name from the function, description from the docstring.
|
||||
* Type hints **are** the input schema. Defaults make arguments optional.
|
||||
* `Annotated[..., Field(...)]` adds descriptions and constraints; `Literal` adds enums.
|
||||
* A Pydantic model parameter is how you take a structured "body".
|
||||
* Bad arguments are rejected for you, with an error the model can read and recover from.
|
||||
* `async def` for I/O, plain `def` for everything else.
|
||||
|
||||
Next up, **Structured Output**: what happens to the value you `return`.
|
||||
Loading…
Reference in a new issue