chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
This commit is contained in:
@@ -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` from **[Testing](../get-started/testing.md)**. 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 declared the capability for you. 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 while the user is still *filling in* a prompt or template; to ask them a question in the *middle* of a tool call, you want **[Elicitation](../handlers/elicitation.md)**. Everything a tool can return besides text is **[Images, audio & icons](media.md)**.
|
||||
@@ -0,0 +1,134 @@
|
||||
# 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 page 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](resources.md)**.
|
||||
|
||||
## 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. **[Tools](tools.md)** shows the same rejection with a `Field(le=50)` constraint.
|
||||
|
||||
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](../get-started/testing.md)** 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. That is everything a server *exposes*. What every handler can read, and do back to the client while it runs, is the next section: **[Inside your handler](../handlers/index.md)**.
|
||||
|
||||
The exact text of the SDK errors you are most likely to meet, what each means, and the one-move fix for each is **[Troubleshooting](../troubleshooting.md)**.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Servers
|
||||
|
||||
An `MCPServer` exposes three primitives to a connected client. They differ by who
|
||||
decides to use them:
|
||||
|
||||
* A **[tool](tools.md)** is an action the *model* picks and calls. This is
|
||||
the page most people want first, and
|
||||
**[Structured Output](structured-output.md)** is its reference companion:
|
||||
everything about the shape of what a tool returns.
|
||||
* A **[resource](resources.md)** is read-only data the *application*
|
||||
chooses to read. **[URI templates](uri-templates.md)** is its reference
|
||||
companion: the full addressing syntax and the path-safety rules.
|
||||
* A **[prompt](prompts.md)** is a message template a *person* invokes by
|
||||
name, from a menu or a slash command.
|
||||
|
||||
Around the three primitives, the rest of what a server declares:
|
||||
|
||||
* **[Completions](completions.md)** is server-side autocomplete for prompt
|
||||
and resource-template arguments.
|
||||
* **[Images, audio & icons](media.md)** covers everything a tool can
|
||||
return besides text, and the icons a client shows next to your server.
|
||||
* **[Handling errors](handling-errors.md)** explains the difference between an
|
||||
error the model can recover from and one it must never see.
|
||||
|
||||
Every page here stands on its own; jump straight to the one you need. If you haven't
|
||||
built a server yet, start with **[First steps](../get-started/first-steps.md)** instead.
|
||||
|
||||
What happens *inside* the functions you register (the `Context`, dependency injection,
|
||||
asking the user for more input mid-call) is the next section,
|
||||
**[Inside your handler](../handlers/index.md)**.
|
||||
@@ -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](structured-output.md)**, where the return annotation *is* the schema.)
|
||||
|
||||
!!! info
|
||||
`ImageContent` and `AudioContent` live in `mcp_types`, right next to the `TextContent`
|
||||
that a plain `str` result becomes (**[Tools](tools.md)**). 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 when the client connects, 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. What happens when a tool *fails* (and who should find out) is **[Handling errors](handling-errors.md)**.
|
||||
@@ -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 reads from a tool:
|
||||
|
||||
* 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 **[Tools](tools.md)** uses to describe a tool's parameters. 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](tools.md)**, 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.
|
||||
|
||||
Server-side autocomplete for a prompt's (or a resource template's) arguments is **[Completions](completions.md)**.
|
||||
@@ -0,0 +1,141 @@
|
||||
# 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.
|
||||
|
||||
The placeholder syntax is [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570): `{+path}` for multi-segment values, `{?q,lang}` for optional query parameters, and more. The SDK also applies path-safety checks to extracted values by default. See **[URI templates and path safety](uri-templates.md)** for the full reference.
|
||||
|
||||
`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](../handlers/context.md)** page 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](../client/index.md)**.
|
||||
|
||||
## 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.
|
||||
|
||||
The third primitive, the one a person picks from a menu, is **[Prompts](prompts.md)**.
|
||||
@@ -0,0 +1,245 @@
|
||||
# Structured Output
|
||||
|
||||
A tool that returns a plain `str` produces the result twice: as text in `content`, and as `{"result": "..."}` in `structured_content`.
|
||||
|
||||
This page 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 it builds from your parameters (**[Tools](tools.md)** covers that one):
|
||||
|
||||
```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](../advanced/low-level-server.md)**.
|
||||
|
||||
## 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](resources.md)**.
|
||||
@@ -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](structured-output.md)** page 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.
|
||||
|
||||
**[Structured Output](structured-output.md)** is what happens to the value you `return`.
|
||||
@@ -0,0 +1,269 @@
|
||||
# URI templates and path safety
|
||||
|
||||
This is the reference for the URI-template syntax that
|
||||
[`@mcp.resource`](resources.md) accepts, and for the
|
||||
path-safety policy the SDK applies to extracted values. For an
|
||||
introduction to what resources are and when to use them, start with
|
||||
**[Resources](resources.md)**; this page assumes you're already comfortable declaring a
|
||||
resource and want the full operator set, the security knobs, or the
|
||||
low-level wiring.
|
||||
|
||||
The template syntax is [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570).
|
||||
The SDK supports a subset chosen for matching incoming `resources/read`
|
||||
URIs, plus a security layer that rejects values that would resolve
|
||||
outside the directory you intend to serve. For the protocol-level
|
||||
details (message formats, lifecycle, pagination) see the
|
||||
[MCP resources specification](https://modelcontextprotocol.io/specification/latest/server/resources).
|
||||
|
||||
## The full operator set
|
||||
|
||||
The plain placeholder, `{user_id}`, is the one **[Resources](resources.md)** introduces. There are four more
|
||||
operator forms; here they are on one server so you can see them next to
|
||||
each other:
|
||||
|
||||
```python title="server.py" hl_lines="16-17 22-23 28-29 34-35 40-41"
|
||||
--8<-- "docs_src/uri_templates/tutorial001.py"
|
||||
```
|
||||
|
||||
Each highlighted decorator is a different way of carving up the URI.
|
||||
The sections below walk them top to bottom.
|
||||
|
||||
### Simple expansion: `{name}`
|
||||
|
||||
`books://{isbn}` is the plain, everyday form. The placeholder maps to
|
||||
the `isbn` parameter, so a client reading `books://978-0441172719` calls
|
||||
`get_book("978-0441172719")`.
|
||||
|
||||
A plain `{name}` stops at the first `/`. `books://978/extra` does not
|
||||
match because the slash after `978` ends the capture and `/extra` is
|
||||
left over.
|
||||
|
||||
### Type conversion
|
||||
|
||||
Extracted values arrive as strings, but you can declare a more specific
|
||||
type and the SDK will convert. `orders://{order_id}` lands in a function
|
||||
whose parameter is `order_id: int`, so reading `orders://12345` calls
|
||||
`get_order(12345)`, not `get_order("12345")`. The handler does
|
||||
arithmetic on it (`order_id + 1`) without a cast.
|
||||
|
||||
### Multi-segment paths: `{+name}`
|
||||
|
||||
To capture a value that contains slashes, use `{+name}`. With
|
||||
`manuals://{+path}`:
|
||||
|
||||
* `manuals://returns.md` gives `path = "returns.md"`
|
||||
* `manuals://printing/setup.md` gives `path = "printing/setup.md"`
|
||||
|
||||
Reach for `{+name}` whenever the value is hierarchical: filesystem
|
||||
paths, nested object keys, URL paths you're proxying.
|
||||
|
||||
### Query parameters: `{?a,b,c}`
|
||||
|
||||
`reviews://{isbn}{?limit,sort}` puts `limit` and `sort` after the `?`.
|
||||
The path identifies *which* book; the query tunes *how* you read it.
|
||||
|
||||
Query params are matched leniently: order doesn't matter, extras are
|
||||
ignored, and omitted params fall through to your function defaults. So
|
||||
`reviews://978-0441172719` uses `limit=10, sort="newest"`, and
|
||||
`reviews://978-0441172719?sort=top` overrides only `sort`.
|
||||
|
||||
### Path segments as a list: `{/name*}`
|
||||
|
||||
If you want each path segment as a separate list item rather than one
|
||||
string with slashes, use `{/name*}`. With `shelves://browse{/path*}`, a
|
||||
client reading `shelves://browse/fiction/sci-fi` calls
|
||||
`browse_shelf(["fiction", "sci-fi"])`.
|
||||
|
||||
### Template reference
|
||||
|
||||
The most common patterns:
|
||||
|
||||
| Pattern | Example input | You get |
|
||||
|--------------|-----------------------|-------------------------|
|
||||
| `{name}` | `alice` | `"alice"` |
|
||||
| `{name}` | `docs/intro.md` | *no match* (stops at `/`) |
|
||||
| `{+path}` | `docs/intro.md` | `"docs/intro.md"` |
|
||||
| `{.ext}` | `.json` | `"json"` |
|
||||
| `{/segment}` | `/v2` | `"v2"` |
|
||||
| `{?key}` | `?key=value` | `"value"` |
|
||||
| `{?a,b}` | `?a=1&b=2` | `"1"`, `"2"` |
|
||||
| `{/path*}` | `/a/b/c` | `["a", "b", "c"]` |
|
||||
|
||||
### What the parser rejects
|
||||
|
||||
A few template shapes are caught up front rather than failing on the
|
||||
first request. `@mcp.resource` parses the template when the decorator
|
||||
runs, so none of these ever reach a running server.
|
||||
|
||||
`UriTemplate.parse()` raises `InvalidUriTemplate` for:
|
||||
|
||||
* **Two variables with nothing between them.** `manuals://{+path}{ext}`
|
||||
is rejected: matching can't tell where `path` ends and `ext` begins.
|
||||
Put a literal between them (`manuals://{+path}/{ext}`), or use an
|
||||
operator that supplies its own delimiter. `manuals://{+path}{.ext}`
|
||||
is accepted because `{.ext}` contributes the `.` itself.
|
||||
* **More than one multi-segment variable.** At most one of `{+var}`,
|
||||
`{#var}`, or an exploded variable (`{/var*}`, `{.var*}`, `{;var*}`)
|
||||
per template. Two are inherently ambiguous: there is no principled
|
||||
way to decide which one absorbs an extra segment.
|
||||
* **The usual syntax errors**: an unclosed brace, a variable name used
|
||||
twice, or an RFC 6570 feature the SDK doesn't support, such as the
|
||||
`{var:3}` prefix modifier or the `{?vars*}` query explode.
|
||||
|
||||
On top of that, `@mcp.resource` raises `ValueError` when a handler
|
||||
parameter is bound to a query variable in the template's trailing
|
||||
`{?...}`/`{&...}` run but has no Python default. Those variables are
|
||||
matched leniently (a client may leave any of them out), so a parameter
|
||||
without a default would only surface as an opaque internal error on the
|
||||
first request that omits it. `reviews://{isbn}{?limit,sort}` in the
|
||||
server above is the well-formed version: `limit` and `sort` both carry
|
||||
defaults.
|
||||
|
||||
## Security
|
||||
|
||||
Template parameters come from the client. If they flow into filesystem
|
||||
or database operations unchecked, values like `../../etc/passwd` can
|
||||
resolve outside the directory you intended to serve.
|
||||
|
||||
### What the SDK checks by default
|
||||
|
||||
Before your handler runs, the SDK rejects any parameter that:
|
||||
|
||||
* would escape its starting directory via `..` components
|
||||
* looks like an absolute path (`/etc/passwd`, `C:\Windows`) or a
|
||||
Windows drive-relative one (`C:foo`). A drive-relative value and a
|
||||
namespaced identifier like `x:y` are indistinguishable as strings,
|
||||
so any single-letter-plus-colon value is rejected by default;
|
||||
exempt the parameter if it legitimately receives such values
|
||||
* contains a null byte (`\x00`)
|
||||
|
||||
The `..` check is component-based, not a substring scan. Values like
|
||||
`v1.0..v2.0` or `HEAD~3..HEAD` pass because `..` is not a standalone
|
||||
path segment there.
|
||||
|
||||
These checks apply to the decoded value, so they catch traversal
|
||||
regardless of how it was encoded in the URI (`../etc`, `..%2Fetc`,
|
||||
`%2E%2E/etc`, `..%5Cetc`, `%00` all get caught).
|
||||
|
||||
!!! check
|
||||
Read `manuals://../etc/passwd` from the server above and the request
|
||||
is rejected outright: template matching stops at the first failure,
|
||||
so no later (potentially more permissive) template is tried as a
|
||||
fallback. The client sees the same `-32602` "Unknown resource" error
|
||||
it would for a URI that matches no template at all, and
|
||||
`read_manual` never runs.
|
||||
|
||||
### Filesystem handlers: use safe_join
|
||||
|
||||
The built-in checks stop the common cases but can't know your sandbox
|
||||
boundary. For filesystem access, use `safe_join` to resolve the path
|
||||
and verify it stays inside your base directory:
|
||||
|
||||
```python title="server.py" hl_lines="4 14"
|
||||
--8<-- "docs_src/uri_templates/tutorial002.py"
|
||||
```
|
||||
|
||||
`safe_join` catches symlink escapes, `..` sequences, and absolute-path
|
||||
tricks that a simple string check would miss. If the resolved path
|
||||
escapes `DOCS_ROOT`, it raises `PathEscapeError`, which surfaces to the
|
||||
client as a `ResourceError`.
|
||||
|
||||
### When the defaults get in the way
|
||||
|
||||
Sometimes the checks block legitimate values. A catalog-import tool
|
||||
might intentionally receive an absolute path, or a parameter might be a
|
||||
relative reference like `../sibling` that your handler interprets
|
||||
safely without touching the filesystem. Exempt that parameter, or relax
|
||||
the policy for the whole server:
|
||||
|
||||
```python title="server.py" hl_lines="9 16-19"
|
||||
--8<-- "docs_src/uri_templates/tutorial003.py"
|
||||
```
|
||||
|
||||
* `security=ResourceSecurity(exempt_params={"source"})` on the decorator
|
||||
skips the checks for that one parameter on that one resource. The
|
||||
rest of the server keeps the default policy.
|
||||
* `resource_security=` on the `MCPServer` constructor sets the default
|
||||
for every resource. Here `relaxed` turns off the `..` check entirely.
|
||||
|
||||
The configurable checks:
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|-------------------------|---------|-------------------------------------|
|
||||
| `reject_path_traversal` | `True` | Rejects `..` sequences that escape the starting directory |
|
||||
| `reject_absolute_paths` | `True` | Rejects `/foo`, `C:\foo`, UNC paths, and drive-relative `C:foo` (also catches `x:y`) |
|
||||
| `reject_null_bytes` | `True` | Rejects values containing `\x00` |
|
||||
| `exempt_params` | empty | Parameter names to skip checks for |
|
||||
|
||||
These checks are a heuristic pre-filter; for filesystem access,
|
||||
`safe_join` remains the containment boundary.
|
||||
|
||||
!!! tip
|
||||
If your handler can't fulfil the request (the file doesn't exist,
|
||||
the id is unknown), raise an exception. The SDK turns it into an
|
||||
error response. See **[Handling errors](handling-errors.md)** for the difference between a
|
||||
protocol error and a tool error.
|
||||
|
||||
## Resources on the low-level Server
|
||||
|
||||
If you're building on the low-level `Server` (see **[The low-level
|
||||
Server](../advanced/low-level-server.md)**), you register handlers for the `resources/list` and
|
||||
`resources/read` protocol methods directly. There's no decorator; you
|
||||
return the protocol types yourself.
|
||||
|
||||
### Static resources
|
||||
|
||||
For fixed URIs, keep a registry and dispatch on exact match:
|
||||
|
||||
```python title="server.py" hl_lines="18 22 28"
|
||||
--8<-- "docs_src/uri_templates/tutorial004.py"
|
||||
```
|
||||
|
||||
The list handler tells clients what's available; the read handler
|
||||
serves the content. Check your registry first, fall through to
|
||||
templates (below) if you have any, then raise for anything else.
|
||||
|
||||
### Templates
|
||||
|
||||
The template engine `MCPServer` uses lives in `mcp.shared.uri_template`
|
||||
and works on its own. You get the same parsing and matching; you wire
|
||||
up the routing and security policy yourself.
|
||||
|
||||
```python title="server.py" hl_lines="14-17 23-26 30 34 46"
|
||||
--8<-- "docs_src/uri_templates/tutorial005.py"
|
||||
```
|
||||
|
||||
Three things are happening in the highlighted lines:
|
||||
|
||||
* **Parse once, match per request.** `UriTemplate.parse()` builds the
|
||||
template; `template.match(uri)` returns the extracted variables as a
|
||||
`dict`, or `None` if the URI doesn't fit. URL decoding happens inside
|
||||
`match()`; the decoded values are returned as-is without path-safety
|
||||
validation. Values come out as strings: convert them yourself
|
||||
(`int(matched["id"])`, `Path(matched["path"])`).
|
||||
* **Apply the safety checks yourself.** The `..` and absolute-path
|
||||
checks `MCPServer` runs by default live in `mcp.shared.path_security`.
|
||||
`read_manual_safely` calls them before touching `MANUALS`. If a
|
||||
parameter isn't a filesystem path (an ISBN, a search query), skip the
|
||||
checks for that value: you control the policy per handler rather than
|
||||
through a config object.
|
||||
* **List the templates from the same source.** Clients discover
|
||||
templates through `resources/templates/list`. `str(template)` gives
|
||||
back the original template string, so the listing and the matcher
|
||||
share one source of truth.
|
||||
|
||||
## Recap
|
||||
|
||||
* `{name}` matches one segment; `{+name}` keeps the slashes; `{?a,b}`
|
||||
pulls from the query string; `{/name*}` splits segments into a list.
|
||||
* Two variables with nothing between them, or a second multi-segment
|
||||
variable, are rejected at parse time. A parameter bound to a trailing
|
||||
`{?...}`/`{&...}` query variable must declare a Python default.
|
||||
* Annotate the parameter (`order_id: int`) and the SDK converts.
|
||||
* The default security policy rejects `..`, absolute paths, and null
|
||||
bytes before your handler runs; override per resource with
|
||||
`security=ResourceSecurity(...)` or server-wide with
|
||||
`resource_security=`.
|
||||
* For filesystem access, `safe_join` is the containment boundary.
|
||||
* On the low-level `Server`, parse with `UriTemplate.parse()`, match
|
||||
with `.match()`, and apply `mcp.shared.path_security` yourself.
|
||||
Reference in New Issue
Block a user