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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:27 +08:00
commit 49b9bb6724
992 changed files with 161690 additions and 0 deletions
+125
View 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` 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)**.
+134
View File
@@ -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)**.
+30
View File
@@ -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)**.
+108
View 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](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)**.
+150
View 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 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)**.
+141
View File
@@ -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)**.
+245
View File
@@ -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)**.
+172
View 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](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`.
+269
View File
@@ -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.