docs: convert bold cross-references into links, link SEP and RFC mentions (#3017)

This commit is contained in:
Max 2026-06-29 13:07:26 +02:00 • committed by GitHub
commit 24fdd909ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 167 additions and 167 deletions

View file

@ -39,7 +39,7 @@ Add **one** function decorated with `@mcp.completion()`:
### Try it
Drive it with the in-memory `Client`, the same one you use in **Testing**. Call
Drive it with the in-memory `Client`, the same one you use in **[Testing](testing.md)**. Call
`client.complete()` with `ref=PromptReference(name="review_code")` and
`argument={"name": "language", "value": "py"}`:
@ -122,4 +122,4 @@ Drop `context_arguments=` and the same call returns `[]`. The handler can't know
* `context.arguments` holds the already-resolved values; the client supplies them as `context_arguments=`.
* The `completions` capability appears the moment you register the handler. Without it, the request is `Method not found`.
Suggestions help *before* a tool runs. To ask the user a question in the *middle* of one, you want **Elicitation**.
Suggestions help *before* a tool runs. To ask the user a question in the *middle* of one, you want **[Elicitation](elicitation.md)**.

View file

@ -60,13 +60,13 @@ The number is whichever request this happened to be. Call the tool again and it
The injected object is small. Besides `request_id`:
* `await ctx.read_resource(uri)`: read one of the server's **own** resources from inside a tool. The next section.
* `await ctx.report_progress(progress, total, message)`: stream progress back to the caller during a long call. The whole story is in **Progress**.
* `await ctx.elicit(message, schema)` and `await ctx.elicit_url(...)`: pause the tool and ask the user a question. That's **Elicitation**.
* `await ctx.report_progress(progress, total, message)`: stream progress back to the caller during a long call. The whole story is in **[Progress](progress.md)**.
* `await ctx.elicit(message, schema)` and `await ctx.elicit_url(...)`: pause the tool and ask the user a question. That's **[Elicitation](elicitation.md)**.
* `ctx.session`: the server's side of the conversation with this client. Notifications you send to the client live here; the last section uses it.
* `ctx.headers`: the request headers the transport carried, or `None` on stdio. Read a custom header with `(ctx.headers or {}).get("x-...")`. Headers are client-supplied input - fine for a locale or a feature flag, never an identity.
* `ctx.request_context`: the raw per-request record. The field you'll reach for is `lifespan_context`, the object your startup code yielded (see **Lifespan**).
* `ctx.request_context`: the raw per-request record. The field you'll reach for is `lifespan_context`, the object your startup code yielded (see **[Lifespan](lifespan.md)**).
Logging is deliberately not on that list. A server logs with Python's `logging` module, like any other Python program. **Logging** is the short chapter on why.
Logging is deliberately not on that list. A server logs with Python's `logging` module, like any other Python program. **[Logging](logging.md)** is the short chapter on why.
!!! tip
Injection only happens for the function you registered. A helper that your tool calls doesn't get
@ -124,4 +124,4 @@ The siblings are `send_resource_list_changed()`, `send_prompt_list_changed()`, a
* `ctx.session` is the channel back to the client: `send_tool_list_changed()` and its siblings tell it to re-fetch a list you changed.
* Progress reporting and elicitation also start at `Context`; each has its own chapter.
Next: parameters the model never sees, filled by your own functions, in **Dependencies**.
Next: parameters the model never sees, filled by your own functions, in **[Dependencies](dependencies.md)**.

View file

@ -35,7 +35,7 @@ Here is the input schema `tools/list` reports for `reserve_book`:
}
```
One property. Like the `Context` in **The Context**, a resolved parameter is a contract between you and the SDK: `stock` is not in the schema, the model is never told about it, and a client that sends a `stock` value anyway is ignored. The resolver's value is the only one your tool can receive.
One property. Like the `Context` in **[The Context](context.md)**, a resolved parameter is a contract between you and the SDK: `stock` is not in the schema, the model is never told about it, and a client that sends a `stock` value anyway is ignored. The resolver's value is the only one your tool can receive.
That last part is the point. A parameter the model cannot supply is a parameter the model cannot get wrong.
@ -84,16 +84,16 @@ A resolver's parameters resolve exactly like a tool's: another `Resolve(...)`, t
!!! warning
On HTTP transports the `Context` includes `ctx.headers`. Headers are **client-supplied input**,
like any tool argument: fine for a locale or a feature flag, never an identity. Who the caller
is comes from your authorization layer (**Authorization**), not from a header anyone can set.
is comes from your authorization layer (**[Authorization](../advanced/authorization.md)**), not from a header anyone can set.
!!! tip
*Once per call* means exactly that: the next `tools/call` runs `check_stock` again. A resource
that should outlive a request - a database pool, an HTTP client - belongs in **Lifespan**, and
that should outlive a request - a database pool, an HTTP client - belongs in **[Lifespan](lifespan.md)**, and
a resolver can reach it through `ctx.request_context.lifespan_context`.
## Ask when you must
A resolver doesn't have to know the answer. It can return `Elicit(message, Model)` and the SDK asks the user - the **Elicitation** machinery, run for you:
A resolver doesn't have to know the answer. It can return `Elicit(message, Model)` and the SDK asks the user - the **[Elicitation](elicitation.md)** machinery, run for you:
```python title="server.py" hl_lines="26-32 39"
--8<-- "docs_src/dependencies/tutorial003.py"
@ -114,7 +114,7 @@ And if the user won't answer at all - declines the question, or cancels it?
Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline
```
That's the right default for a precondition: no answer, no order. When declining is an outcome your tool wants to handle - skip the backorder but still suggest another title - annotate `ElicitationResult[Backorder]` instead and the tool receives the full accept/decline/cancel outcome to branch on. **Elicitation** shows that form, and everything else about asking: the schema rules, the three answers, the client's side of the conversation.
That's the right default for a precondition: no answer, no order. When declining is an outcome your tool wants to handle - skip the backorder but still suggest another title - annotate `ElicitationResult[Backorder]` instead and the tool receives the full accept/decline/cancel outcome to branch on. **[Elicitation](elicitation.md)** shows that form, and everything else about asking: the schema rules, the three answers, the client's side of the conversation.
## Recap
@ -124,4 +124,4 @@ That's the right default for a precondition: no answer, no order. When declining
* Bad graphs fail at registration with `InvalidSignature`, not mid-call.
* Return `Elicit(message, Model)` to ask the user, only when you have to. Unwrapped annotations abort on decline; `ElicitationResult[T]` lets the tool branch.
Next: what happens when your tool fails, and how to choose who finds out, in **Handling errors**.
Next: what happens when your tool fails, and how to choose who finds out, in **[Handling errors](handling-errors.md)**.

View file

@ -17,7 +17,7 @@ There are two modes:
--8<-- "docs_src/elicitation/tutorial001.py"
```
* The **`Context`** parameter is what gives you `ctx.elicit`; any tool can take one. That object has its own chapter: **The Context**.
* The **`Context`** parameter is what gives you `ctx.elicit`; any tool can take one. That object has its own chapter: **[The Context](context.md)**.
* `AlternativeDate` is the **schema** of the answer you want.
* The tool is `async def`. It has to be: it stops in the middle and waits for a person.
* On any other date the tool returns straight away. It only asks when it has to.
@ -48,7 +48,7 @@ The client gets your message and, next to it, a JSON Schema generated from the m
}
```
That schema is the form. `Field(description=...)` is the label; a default pre-fills the input and makes the field optional. It's the same Pydantic-to-JSON-Schema machinery you already used for a tool's arguments in **Tools**.
That schema is the form. `Field(description=...)` is the label; a default pre-fills the input and makes the field optional. It's the same Pydantic-to-JSON-Schema machinery you already used for a tool's arguments in **[Tools](tools.md)**.
!!! warning
An elicitation schema is not as expressive as a tool's input schema. Flat, primitive fields
@ -95,7 +95,7 @@ A parameter annotated `Annotated[T, Resolve(fn)]` is filled by running `fn` befo
Annotate the unwrapped model (`Annotated[Confirm, Resolve(confirm_delete)]`) instead when the tool doesn't need to branch: it receives the model on accept and the call aborts with an error on decline or cancel.
Asking is only one thing a resolver can do. The general mechanism - dependencies that compute without asking, dependencies of dependencies, what the model can and cannot supply - is the **Dependencies** chapter.
Asking is only one thing a resolver can do. The general mechanism - dependencies that compute without asking, dependencies of dependencies, what the model can and cannot supply - is the **[Dependencies](dependencies.md)** chapter.
## Send the user to a URL
@ -122,17 +122,17 @@ Servers ask. Clients answer by passing an **`elicitation_callback`** to `Client(
* One callback handles both modes. `params` is a union of `ElicitRequestFormParams` and `ElicitRequestURLParams`; `isinstance` is the branch.
* For a URL, you show `params.url` to the user and return the action they chose. Never any `content`.
* For a form, a real application renders `params.requested_schema` and returns the user's input as `content`. This one always says yes with a canned answer, which is exactly the callback you want in a test.
* Passing the callback is also the **capability declaration**: it's how the server learns this client can be asked. The other things a client can answer for a server live in **Client callbacks**.
* Passing the callback is also the **capability declaration**: it's how the server learns this client can be asked. The other things a client can answer for a server live in **[Client callbacks](../client/callbacks.md)**.
!!! info
Elicitation is a request from the *server* to the *client*, and those only exist on a
classic-handshake session, which is why this client passes `mode="legacy"`.
On a **2026-07-28** connection a tool asks by *returning* the question from the call
instead; that flow is **Multi-round-trip requests**.
instead; that flow is **[Multi-round-trip requests](../advanced/multi-round-trip.md)**.
### Try it
Start the form-mode `server.py` (the first one on this page) on Streamable HTTP (**Running your server** has the one-liner), then run the client's `main()` and ask `book_table` for Christmas day.
Start the form-mode `server.py` (the first one on this page) on Streamable HTTP (**[Running your server](../run/index.md)** has the one-liner), then run the client's `main()` and ask `book_table` for Christmas day.
The callback prints the question it was sent:
@ -167,6 +167,6 @@ Now swap in the URL-mode `server.py` and point the same `main()` at `pay_deposit
* `result.action` is `"accept"`, `"decline"` or `"cancel"`; `result.data` exists only on accept.
* `await ctx.elicit_url(message, url, elicitation_id)` is for everything that must not pass through the model; `ctx.session.send_elicit_complete(elicitation_id)` says the out-of-band part is done.
* The client answers with one `elicitation_callback`, branching on the params type; registering it is what declares the capability.
* On a 2026-07-28 connection the server returns the question instead of pushing it; the same callback is fed by **Multi-round-trip requests**.
* On a 2026-07-28 connection the server returns the question instead of pushing it; the same callback is fed by **[Multi-round-trip requests](../advanced/multi-round-trip.md)**.
A tool that can ask is good. A tool that says how far along it is (**Progress**) is next.
A tool that can ask is good. A tool that says how far along it is (**[Progress](progress.md)**) is next.

View file

@ -70,7 +70,7 @@ Hello, World!
**Prompts.** One entry: `summarize`, with a single required `text` argument. Get it with some text and you receive one message with `role: user` and your rendered string as the content. That's all a prompt is: a function that builds messages.
The Inspector ran your server over **stdio**, one of the transports an MCP server can speak. You don't pick one yet; **Running your server** is the chapter for that.
The Inspector ran your server over **stdio**, one of the transports an MCP server can speak. You don't pick one yet; **[Running your server](../run/index.md)** is the chapter for that.
## Capabilities
@ -110,11 +110,11 @@ That dictionary is the server's half of the handshake:
`MCPServer` serves all three primitives, so all three are always declared.
Notice what isn't there. `completions` (argument autocomplete for resource templates and prompts) needs a handler you write, this server doesn't have one, so the capability is absent and a well-behaved client won't ask. That's the rule for everything optional: register the thing and the capability appears; **Completions** proves it.
Notice what isn't there. `completions` (argument autocomplete for resource templates and prompts) needs a handler you write, this server doesn't have one, so the capability is absent and a well-behaved client won't ask. That's the rule for everything optional: register the thing and the capability appears; **[Completions](completions.md)** proves it.
!!! info
`Client(mcp)` is the same in-memory client every example in this tutorial is tested with, and
it's how you'll test yours. It gets a whole chapter: **Testing**.
it's how you'll test yours. It gets a whole chapter: **[Testing](testing.md)**.
## What you did not write
@ -136,4 +136,4 @@ That ratio is the whole point of the SDK.
* The server's **capabilities** are declared for you, and a client only asks for what a server declares.
* `Client(mcp)` connects to the server object in memory: your test harness from day one.
Each primitive now gets its own chapter, starting with the one the model drives: **Tools**.
Each primitive now gets its own chapter, starting with the one the model drives: **[Tools](tools.md)**.

View file

@ -104,13 +104,13 @@ When it can't, raise `ResourceNotFoundError`. The SDK turns it into the protocol
}
```
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**.
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. You saw this in **Tools** with `Field(le=50)`.
Send `get_author` a `title` that isn't a string and the SDK rejects it against the input schema **before** calling you, as the same kind of `is_error=True` tool error the model can read and correct. You saw this in **[Tools](tools.md)** with `Field(le=50)`.
It means a whole class of `raise` statements you don't write: don't re-validate your own type hints.
@ -118,7 +118,7 @@ It means a whole class of `raise` statements you don't write: don't re-validate
Everything on this page is what a **client** sees, and the in-memory `Client` you'll write
tests with sees exactly the same thing. Even `raise_exceptions=True` doesn't turn a tool error
back into a traceback: by the time that flag could act, your exception is already the
`is_error=True` result. Assert on the result. **Testing** covers the pattern.
`is_error=True` result. Assert on the result. **[Testing](testing.md)** covers the pattern.
## Recap
@ -129,4 +129,4 @@ It means a whole class of `raise` statements you don't write: don't re-validate
* Bad arguments are rejected against the schema before your function runs; you don't `raise` for those.
* `from mcp import MCPError`; the error-code constants come from `mcp_types`.
Errors handled. Next: the things your server sets up once, before the first call ever arrives, the **Lifespan**.
Errors handled. Next: the things your server sets up once, before the first call ever arrives, the **[Lifespan](lifespan.md)**.

View file

@ -41,7 +41,7 @@ Nothing new. `ctx` is a **Context** parameter, so the SDK injects it and it neve
`genre` is the only argument the model can pass. The lifespan is your server's business.
`@mcp.resource()` and `@mcp.prompt()` functions can take a `ctx` parameter too, written as a bare `Context` for a reason the next section gets to. Everything `ctx` carries is in **The Context**.
`@mcp.resource()` and `@mcp.prompt()` functions can take a `ctx` parameter too, written as a bare `Context` for a reason the next section gets to. Everything `ctx` carries is in **[The Context](context.md)**.
### It really is typed
@ -99,4 +99,4 @@ Strip the server down to the lifecycle: give `Database` a `connected` flag, flip
* `ctx: Context[AppContext]` makes that access fully typed in tools. Resources and prompts take the bare `Context`.
* No `lifespan=` means an empty `dict`, never `None`.
Next: tools that return more than text, **Media**.
Next: tools that return more than text, **[Media](media.md)**.

View file

@ -2,7 +2,7 @@
Log from a tool the way you log from any other Python function: with the standard library.
MCP has a protocol-level **logging capability**: a server could push its log messages to the client as notifications, through methods on the `Context` object. The 2026-07-28 revision of the spec **deprecates that capability and does not replace it**, so this tutorial doesn't teach it. The full list of what's deprecated and what to do instead is in **Deprecated features**.
MCP has a protocol-level **logging capability**: a server could push its log messages to the client as notifications, through methods on the `Context` object. The 2026-07-28 revision of the spec **deprecates that capability and does not replace it**, so this tutorial doesn't teach it. The full list of what's deprecated and what to do instead is in **[Deprecated features](../advanced/deprecated.md)**.
What you do instead is what you do in every other Python program: the standard library.
@ -65,7 +65,7 @@ went to standard error: the terminal, not the wire.
!!! info
If what you actually want is *tracing* (every request, how long it took, whether it failed), you
don't want log lines, you want spans. Your server already emits them: the SDK traces every
message with OpenTelemetry out of the box. See **OpenTelemetry**.
message with OpenTelemetry out of the box. See **[OpenTelemetry](../advanced/opentelemetry.md)**.
## Recap
@ -75,4 +75,4 @@ went to standard error: the terminal, not the wire.
* Standard error is yours; stdout belongs to the protocol. Never `print()` in a stdio server.
* `MCPServer(..., log_level="DEBUG")` sets the level, and a logging configuration you made first is left alone.
Next: the in-memory client that has been running every example on these pages, and how to point it at your own server, in **Testing**.
Next: the in-memory client that has been running every example on these pages, and how to point it at your own server, in **[Testing](testing.md)**.

View file

@ -26,11 +26,11 @@ result.structured_content # None
Two things to notice:
* `data` is base64. You returned raw `bytes`; the SDK did the encoding.
* `structured_content` is `None`. An `Image` is content for the model to look at, not data for the application to parse: there is no output schema. (Contrast **Structured Output**, where the return annotation *is* the schema.)
* `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`
you met in **Tools**. A tool result is a list of content blocks; `Image` and `Audio` are
you met in **[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
@ -105,4 +105,4 @@ A tool's icons are on the `Tool` object from `tools/list`, a resource's on the `
* An `Icon` is a pointer: a `src` URI plus optional `mime_type`, `sizes`, and `theme`.
* `icons=[...]` works on the server, on tools, on resources, and on prompts, and clients find them on the matching objects.
That is everything a tool can put *into* a result. Helping the user fill in a prompt's or a resource template's arguments *before* anything runs is **Completions**.
That is everything a tool can put *into* a result. Helping the user fill in a prompt's or a resource template's arguments *before* anything runs is **[Completions](completions.md)**.

View file

@ -18,7 +18,7 @@ Three arguments, and you decide what they mean:
* `total`: how much there is in total, if you know. Optional.
* `message`: one human-readable line about *this* step. Optional.
`ctx` is injected because of its type hint and the model never sees it: `import_catalog`'s input schema has a single property, `urls`. **The Context** chapter is all about that object; progress is one of the things it gives you.
`ctx` is injected because of its type hint and the model never sees it: `import_catalog`'s input schema has a single property, `urls`. **[The Context](context.md)** chapter is all about that object; progress is one of the things it gives you.
## Listen for it from the client
@ -51,7 +51,7 @@ anyio.run(main)
The callback is an `async` function taking exactly what the server reported: `progress`, `total`, `message`.
!!! info
`Client(mcp)` connects straight to the server object, in memory, the same client the **Testing**
`Client(mcp)` connects straight to the server object, in memory, the same client the **[Testing](testing.md)**
chapter is built on. `progress_callback` is the same parameter whatever transport the `Client`
uses; the *timing* you are about to see is the in-memory connection's. It runs your callback
inline, so every report lands before `call_tool` returns. Over a real transport the
@ -114,4 +114,4 @@ The callback receives `total=None`. A client can still show *activity* ("3 impor
* No callback on the call means `report_progress` does nothing. Report unconditionally.
* Omit `total` when you don't know it; the callback gets `None`.
Progress is what a running tool shows the *user*. The lines it logs for *you*, the person operating the server, are a different channel: **Logging** is next.
Progress is what a running tool shows the *user*. The lines it logs for *you*, the person operating the server, are a different channel: **[Logging](logging.md)** is next.

View file

@ -116,7 +116,7 @@ Notice the last one. Pre-filling an `assistant` turn is how you steer the model'
```
* `title="Code review"` is the human-readable name, exactly like a tool's `title`.
* `Annotated[str, Field(description=...)]` is the same pattern you used in **Tools**. Here the description lands on the argument instead of in a schema.
* `Annotated[str, Field(description=...)]` is the same pattern you used in **[Tools](tools.md)**. 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:
@ -134,7 +134,7 @@ The `prompts/list` entry now carries everything a client needs to draw a good fo
```
!!! info
If you have read **Tools**, you already know everything on this page. Same decorator, same
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).
@ -147,4 +147,4 @@ The `prompts/list` entry now carries everything a client needs to draw a good fo
* `title=` and `Field(description=...)` are what a client puts in its UI.
* A missing required argument fails the whole request. There is no per-prompt error result.
Next up: the one extra parameter a tool, resource or prompt can ask the SDK for, **The Context**.
Next up: the one extra parameter a tool, resource or prompt can ask the SDK for, **[The Context](context.md)**.

View file

@ -92,9 +92,9 @@ Notice the `uri` in the result. It is the **concrete** URI the client asked for,
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: `{+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](../advanced/uri-templates.md)** for the full reference.
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](../advanced/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** chapter covers what it gives you.
`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](context.md)** chapter covers what it gives you.
## What you return
@ -127,7 +127,7 @@ The same rule applies to anything else JSON-serialisable: a list, a Pydantic mod
`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**.
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
@ -138,4 +138,4 @@ A client can also **subscribe** to a resource and be notified when it changes; t
* `str` becomes text, `bytes` becomes a base64 blob, anything else becomes JSON text. `mime_type=` is how you label it.
* Tools are for the model to act. Resources are for the application to read.
Next: the third primitive, the one a person picks from a menu, **Prompts**.
Next: the third primitive, the one a person picks from a menu, **[Prompts](prompts.md)**.

View file

@ -1,6 +1,6 @@
# Structured Output
In **Tools** you returned a `str` and the result came back twice: as text in `content`, and as `{"result": "..."}` in `structured_content`.
In **[Tools](tools.md)** you returned a `str` and the result came back twice: as text in `content`, and as `{"result": "..."}` in `structured_content`.
This chapter is about that second channel: where it comes from, every shape it can take, and how the SDK keeps it honest.
@ -14,7 +14,7 @@ The short version: **the return type annotation is the output schema**. You alre
The line that matters is the signature: `-> int`.
Because of it, the tool the SDK sends during `tools/list` carries an `output_schema` next to the input schema you met in **Tools**:
Because of it, the tool the SDK sends during `tools/list` carries an `output_schema` next to the input schema you met in **[Tools](tools.md)**:
```json
{
@ -232,7 +232,7 @@ There is one way to end up unstructured without asking for it: return a class th
!!! 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**.
application can see but the model can't)? That's **[The low-level Server](../advanced/low-level-server.md)**.
## Recap
@ -242,4 +242,4 @@ There is one way to end up unstructured without asking for it: return a class th
* 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**.
You now own everything a tool can say back. Next, the second primitive: **[Resources](resources.md)**.

View file

@ -79,7 +79,7 @@ Two different things can go wrong, and this flag only touches one of them.
An exception inside one of **your tools** is not a protocol failure. It becomes a normal result with
`is_error=True`, and the model reads the message. `raise_exceptions` doesn't change that: with or
without it, `call_tool` returns the same `is_error=True` result. There's a whole chapter on it:
**Handling errors**.
**[Handling errors](handling-errors.md)**.
A failure **outside** a tool body is different. On the connection `Client(mcp)` gives you, the
server sanitises it into a generic `"Internal server error"` before the client sees it. You should
@ -103,4 +103,4 @@ example file is exercised by the SDK's own test suite through exactly this clien
same tool the SDK uses on itself.
The tutorial ends here. Putting your tested server in front of a real client, over a real
transport, is **Running your server**.
transport, is **[Running your server](../run/index.md)**.

View file

@ -49,7 +49,7 @@ result.structured_content # {'result': "Found 3 books matching 'dune' (showing
`content` is the text the **model** reads. `structured_content` is typed data for the **client application**. It's there because you declared the return type as `-> str`.
Don't worry about `structured_content` yet. Return real Python objects from your tools and the right thing happens; the **Structured Output** chapter is all about it.
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)** chapter is all about it.
### Try it
@ -169,4 +169,4 @@ A well-behaved client uses them to decide things like *"do I need to ask the use
* Bad arguments are rejected for you, with an error the model can read and recover from.
* `async def` for I/O, plain `def` for everything else.
Next up, **Structured Output**: what happens to the value you `return`.
Next up, **[Structured Output](structured-output.md)**: what happens to the value you `return`.