diff --git a/docs/advanced/authorization.md b/docs/advanced/authorization.md index 2afb3d5a0..9b5a32a4e 100644 --- a/docs/advanced/authorization.md +++ b/docs/advanced/authorization.md @@ -32,11 +32,11 @@ The SDK has no opinion about what a valid token looks like. You tell it, by impl !!! tip `examples/servers/simple-auth/` in the SDK repository has an `IntrospectionTokenVerifier` that calls - a real authorization server's RFC 7662 endpoint. It's the shape most production verifiers take. + a real authorization server's [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) endpoint. It's the shape most production verifiers take. ## What you get over HTTP -Authorization lives in HTTP headers, so it exists only on the HTTP transports. Run it on the one you deploy: `mcp.run(transport="streamable-http")` puts it on `http://127.0.0.1:8000/mcp`, and **Running your server** has the rest. The app now has two routes: +Authorization lives in HTTP headers, so it exists only on the HTTP transports. Run it on the one you deploy: `mcp.run(transport="streamable-http")` puts it on `http://127.0.0.1:8000/mcp`, and **[Running your server](../run/index.md)** has the rest. The app now has two routes: ```text /mcp @@ -47,7 +47,7 @@ You registered one tool. The second route is the SDK's. ### Discovery -`GET` that well-known path and you get **RFC 9728 Protected Resource Metadata**, built straight from your `AuthSettings`: +`GET` that well-known path and you get **[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata**, built straight from your `AuthSettings`: ```json { @@ -109,15 +109,15 @@ To watch all three parties move, run `examples/servers/simple-auth/` from the SD server inside your MCP server. It predates the AS/RS separation that the MCP authorization spec is built around. New servers should not reach for it. -An authorization server can also accept an enterprise identity provider's signed assertion in place of a user clicking through a consent screen, and the SDK supports both sides of that exchange. The grant, and the client that presents it, is **Identity assertion**. +An authorization server can also accept an enterprise identity provider's signed assertion in place of a user clicking through a consent screen, and the SDK supports both sides of that exchange. The grant, and the client that presents it, is **[Identity assertion](identity-assertion.md)**. ## Recap * Over Streamable HTTP your server is an OAuth 2.1 **resource server**: it verifies tokens, it never issues them. * `TokenVerifier` is the whole integration surface: one async method, token in, `AccessToken | None` out. * `token_verifier=` and `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` always travel together. -* The SDK publishes RFC 9728 Protected Resource Metadata at `/.well-known/oauth-protected-resource/...` and answers unauthenticated requests with a 401 whose `WWW-Authenticate` header points at it. That is the entire discovery story. +* The SDK publishes [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata at `/.well-known/oauth-protected-resource/...` and answers unauthenticated requests with a 401 whose `WWW-Authenticate` header points at it. That is the entire discovery story. * `get_access_token()` in any handler is who's calling. * Authorization is an HTTP concern. `stdio` and the in-memory client never see it. -The other side of the handshake, a client that discovers your authorization server and fetches the token for you, is **OAuth clients**. +The other side of the handshake, a client that discovers your authorization server and fetches the token for you, is **[OAuth clients](oauth-clients.md)**. diff --git a/docs/advanced/deprecated.md b/docs/advanced/deprecated.md index 5bff0e955..18bcc7946 100644 --- a/docs/advanced/deprecated.md +++ b/docs/advanced/deprecated.md @@ -8,16 +8,16 @@ The table below names each deprecated feature, why it is going away, and the rep | Deprecated | Why | What you do instead | |---|---|---| -| **Roots**: `ctx.session.list_roots()`, `client.send_roots_list_changed()`, the `list_roots_callback=` you pass to `Client(...)` | [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) retires the capability. | Take the paths as ordinary tool arguments or resource URIs, or embed a `ListRootsRequest` in an `InputRequiredResult` (see **Multi-round-trip requests**). | -| **Server-initiated sampling**: `ctx.session.create_message()`, the `sampling_callback=` you pass to `Client(...)` | SEP-2577 retires the capability. | Return `InputRequiredResult` and let the client retry the call (see **Multi-round-trip requests**). | -| **Protocol logging**: `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | SEP-2577 retires the capability. Nothing in-protocol replaces it. | Ordinary `import logging` to stderr (see **Logging**). | +| **Roots**: `ctx.session.list_roots()`, `client.send_roots_list_changed()`, the `list_roots_callback=` you pass to `Client(...)` | [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) retires the capability. | Take the paths as ordinary tool arguments or resource URIs, or embed a `ListRootsRequest` in an `InputRequiredResult` (see **[Multi-round-trip requests](multi-round-trip.md)**). | +| **Server-initiated sampling**: `ctx.session.create_message()`, the `sampling_callback=` you pass to `Client(...)` | SEP-2577 retires the capability. | Return `InputRequiredResult` and let the client retry the call (see **[Multi-round-trip requests](multi-round-trip.md)**). | +| **Protocol logging**: `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | SEP-2577 retires the capability. Nothing in-protocol replaces it. | Ordinary `import logging` to stderr (see **[Logging](../tutorial/logging.md)**). | | **`ping`**: `client.send_ping()` | **Removed** from the protocol, not merely deprecated. There is no `ping` method in 2026-07-28. | Nothing. It only works against a `mode="legacy"` connection. | -| **Client->server progress**: `client.send_progress_notification()` | 2026-07-28 makes progress server->client only. | Nothing to send. Your *server* reports progress with `ctx.report_progress()` (see **Progress**). | +| **Client->server progress**: `client.send_progress_notification()` | 2026-07-28 makes progress server->client only. | Nothing to send. Your *server* reports progress with `ctx.report_progress()` (see **[Progress](../tutorial/progress.md)**). | Three things fall out of that table: * Roots, sampling, and logging go together. One proposal, **SEP-2577**, deprecates all three capabilities at once. -* Sampling and roots share a deeper problem: they are places a **server** sends a **request** to the **client**. That whole direction is what 2026-07-28 replaces with **Multi-round-trip requests**. It is the standalone RPC methods (`sampling/createMessage`, `roots/list`, and push-style `elicitation/create`) that are gone; the `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` payload types survive, embedded in `InputRequiredResult.input_requests`, and on the client they hit the same callbacks. +* Sampling and roots share a deeper problem: they are places a **server** sends a **request** to the **client**. That whole direction is what 2026-07-28 replaces with **[Multi-round-trip requests](multi-round-trip.md)**. It is the standalone RPC methods (`sampling/createMessage`, `roots/list`, and push-style `elicitation/create`) that are gone; the `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` payload types survive, embedded in `InputRequiredResult.input_requests`, and on the client they hit the same callbacks. * `ping` is the odd one out. The protocol does not deprecate it, it removes it. The SDK method still warns (its message says *removed*, not *deprecated*) and calling it on a modern connection answers with *"Method not found"*. ## Deprecated is advisory @@ -81,8 +81,8 @@ That is the whole API. There is no per-method switch, and you don't want one: th ## Recap -* The 2026-07-28 spec deprecates **roots**, server-initiated **sampling**, and protocol **logging** (all SEP-2577), restricts **progress** to server-to-client, and removes **`ping`**. -* The replacement column points you onward: **Multi-round-trip requests** for sampling and roots, **Logging** for logging, **Progress** for progress. `ping` needs nothing at all. +* The 2026-07-28 spec deprecates **roots**, server-initiated **sampling**, and protocol **logging** (all [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), restricts **progress** to server-to-client, and removes **`ping`**. +* The replacement column points you onward: **[Multi-round-trip requests](multi-round-trip.md)** for sampling and roots, **[Logging](../tutorial/logging.md)** for logging, **[Progress](../tutorial/progress.md)** for progress. `ping` needs nothing at all. * Deprecated is advisory: no wire changes, everything keeps working against pre-2026 sessions, and you get a visible `MCPDeprecationWarning` (a `UserWarning`, so it is on by default). * Sampling and roots additionally need a back-channel that a 2026-07-28 session does not have. On a modern connection they warn and then they raise. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silences the whole category; `"error::mcp.MCPDeprecationWarning"` in pytest turns it into a test failure. diff --git a/docs/advanced/extensions.md b/docs/advanced/extensions.md index 6ca164228..5a6d7d524 100644 --- a/docs/advanced/extensions.md +++ b/docs/advanced/extensions.md @@ -4,7 +4,7 @@ An **extension** is an opt-in bundle of MCP behaviour behind one identifier. It can contribute tools, resources, and new request methods, and it can wrap `tools/call`. The server advertises it under `capabilities.extensions`, the client opts in the same way, -and nothing changes for anyone who didn't ask for it. That is the contract (SEP-2133), and +and nothing changes for anyone who didn't ask for it. That is the contract ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), and it has one golden rule: **extensions are off by default**. ## Using an extension diff --git a/docs/advanced/identity-assertion.md b/docs/advanced/identity-assertion.md index 5a48c13c7..7e7318361 100644 --- a/docs/advanced/identity-assertion.md +++ b/docs/advanced/identity-assertion.md @@ -1,25 +1,25 @@ # Identity assertion -Every provider in **OAuth clients** starts by asking the MCP server a question: *which authorization server do you trust?* It follows the answer wherever it points, and then either a person signs in or a pre-shared secret stands in for one. +Every provider in **[OAuth clients](oauth-clients.md)** starts by asking the MCP server a question: *which authorization server do you trust?* It follows the answer wherever it points, and then either a person signs in or a pre-shared secret stands in for one. An enterprise wants neither decided per server. It already runs an identity provider (Okta, Microsoft Entra ID, your own); the user already signed in to it this morning; and it is the one place the security team wants to decide who may reach what. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), the **Enterprise-Managed Authorization** extension, moves the decision there. The IdP signs a short-lived JWT, an **Identity Assertion JWT Authorization Grant**, the **ID-JAG**: a statement that *this user*, through *this client*, may reach *this MCP server*. The client trades it for an ordinary access token. No browser, no consent screen, no dynamic registration. -This chapter is both ends of that trade. The MCP server itself never changes: it is still the resource server from **Authorization**, checking whatever token shows up. +This chapter is both ends of that trade. The MCP server itself never changes: it is still the resource server from **[Authorization](authorization.md)**, checking whatever token shows up. ## Two token requests -Two different authorities are in play, and naming them apart is most of understanding this page. The **enterprise IdP** is your organization's identity provider: it knows who the employee is, it is where policy lives, and it issues the ID-JAG. The SDK never talks to it. The **MCP authorization server** is the same party it was in **Authorization**: the issuer named in the MCP server's metadata, the thing that mints the tokens that MCP server accepts. In the flows you already know, those two roles are usually one box. Here they are two, and the whole grant is the second agreeing to trust the first. +Two different authorities are in play, and naming them apart is most of understanding this page. The **enterprise IdP** is your organization's identity provider: it knows who the employee is, it is where policy lives, and it issues the ID-JAG. The SDK never talks to it. The **MCP authorization server** is the same party it was in **[Authorization](authorization.md)**: the issuer named in the MCP server's metadata, the thing that mints the tokens that MCP server accepts. In the flows you already know, those two roles are usually one box. Here they are two, and the whole grant is the second agreeing to trust the first. The client makes one token request to each. -1. **To the enterprise IdP.** The client trades the user's sign-in (their OpenID Connect ID token) for the ID-JAG. This is an RFC 8693 token exchange, it is entirely your IdP's API, and **the SDK does not make it**. You do, inside one async callback. It is also where the policy decision happens: an IdP that says no never issues the ID-JAG, and there is nothing to present. -2. **To the MCP authorization server.** The client presents the ID-JAG under the RFC 7523 `jwt-bearer` grant (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, the ID-JAG as `assertion`) and receives the access token. **This is the request the SDK makes**, and accepting it is the one thing this page adds to an authorization server. +1. **To the enterprise IdP.** The client trades the user's sign-in (their OpenID Connect ID token) for the ID-JAG. This is an [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange, it is entirely your IdP's API, and **the SDK does not make it**. You do, inside one async callback. It is also where the policy decision happens: an IdP that says no never issues the ID-JAG, and there is nothing to present. +2. **To the MCP authorization server.** The client presents the ID-JAG under the [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) `jwt-bearer` grant (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, the ID-JAG as `assertion`) and receives the access token. **This is the request the SDK makes**, and accepting it is the one thing this page adds to an authorization server. Everything below is the second request: the client that sends it and the authorization server that answers it. ## The client -**`IdentityAssertionOAuthProvider`** lives in `mcp.client.auth.extensions.identity_assertion`. Like every provider in **OAuth clients** it is an `httpx.Auth`: construct one, put it on `auth=`, hand the `httpx.AsyncClient` to the transport. +**`IdentityAssertionOAuthProvider`** lives in `mcp.client.auth.extensions.identity_assertion`. Like every provider in **[OAuth clients](oauth-clients.md)** it is an `httpx.Auth`: construct one, put it on `auth=`, hand the `httpx.AsyncClient` to the transport. ```python title="client.py" hl_lines="49-50 53-61" --8<-- "docs_src/identity_assertion/tutorial001.py" @@ -27,7 +27,7 @@ Everything below is the second request: the client that sends it and the authori Read it from the bottom. -* `main()` is the `main()` from **OAuth clients**, line for line. That is the point: once the provider exists, nothing downstream knows which grant produced the token. +* `main()` is the `main()` from **[OAuth clients](oauth-clients.md)**, line for line. That is the point: once the provider exists, nothing downstream knows which grant produced the token. * The provider takes what the other providers cannot discover: a `client_id` and `client_secret` somebody **pre-registered** with the authorization server, that authorization server's `issuer`, and `assertion_provider`, an async callback that returns a fresh ID-JAG on demand. * `storage` is the same `TokenStorage` protocol. Only the two token methods are ever called; there is no dynamic registration here, so there is no `client_info` to remember. @@ -35,7 +35,7 @@ Read it from the bottom. `fetch_id_jag(audience, resource)` is the only code you write. It is awaited once per token exchange, never at construction, and only *after* the authorization server's metadata has been fetched and validated, so a misconfigured issuer never leaks an assertion. Its two arguments are two of the claims the ID-JAG must be minted with: `audience` is the authorization server's issuer (the ID-JAG `aud`) and `resource` is the MCP server's canonical identifier (the ID-JAG `resource`). The third is one you already hold: the ID-JAG's `client_id` claim must name the `client_id` you gave the provider, or the authorization server refuses the exchange. -`idp_issue_id_jag` above it is **not your code**. It stands in for the identity provider, signing the assertion in-process so the file is complete and you can read every claim an ID-JAG carries. A real `fetch_id_jag` makes the first token request of the previous section instead: an RFC 8693 token exchange against your IdP, defined by the Identity Assertion JWT Authorization Grant draft that SEP-990 profiles. The signed-in user's ID token goes in as the `subject_token`, the `requested_token_type` is the ID-JAG's own URN (`urn:ietf:params:oauth:token-type:id-jag`), `audience` and `resource` pass straight through, and the response carries the ID-JAG. That exchange, under those names, is what to look for in your IdP's documentation. +`idp_issue_id_jag` above it is **not your code**. It stands in for the identity provider, signing the assertion in-process so the file is complete and you can read every claim an ID-JAG carries. A real `fetch_id_jag` makes the first token request of the previous section instead: an [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange against your IdP, defined by the Identity Assertion JWT Authorization Grant draft that [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) profiles. The signed-in user's ID token goes in as the `subject_token`, the `requested_token_type` is the ID-JAG's own URN (`urn:ietf:params:oauth:token-type:id-jag`), `audience` and `resource` pass straight through, and the response carries the ID-JAG. That exchange, under those names, is what to look for in your IdP's documentation. !!! tip A fresh ID-JAG is requested for every exchange, and that is the point: it is a single-use, @@ -44,7 +44,7 @@ Read it from the bottom. ### The issuer is configuration -Here is the inversion. `OAuthClientProvider` asks the resource server which authorization server to use and follows the answer wherever it points. This provider refuses to: `issuer` is required, the RFC 8414 metadata is fetched from that issuer's own well-known path, the token endpoint must be on that issuer's origin, and the resource server is never asked anything. +Here is the inversion. `OAuthClientProvider` asks the resource server which authorization server to use and follows the answer wherever it points. This provider refuses to: `issuer` is required, the [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) metadata is fetched from that issuer's own well-known path, the token endpoint must be on that issuer's origin, and the resource server is never asked anything. The extension does not demand this; it is a deliberately stricter choice. This client carries two things worth stealing, a pre-registered secret and an audience-bound assertion, and a client that let a compromised MCP server steer it to an attacker's authorization server would post both to it. Pinning the issuer at construction deletes that conversation. @@ -59,7 +59,7 @@ The extension does not demand this; it is a deliberately stricter choice. This c ### A confidential client -`client_secret` is required; the constructor raises `ValueError` without one. The IETF profile underneath SEP-990 reserves this grant for confidential clients, SEP-990 requires the client to authenticate, and this SDK enforces both by insisting on a shared secret. `token_endpoint_auth_method` picks where it travels: `client_secret_post` (the default, in the form body) or `client_secret_basic` (an HTTP Basic header). The profile also permits `private_key_jwt`; this provider does not support it. +`client_secret` is required; the constructor raises `ValueError` without one. The IETF profile underneath [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) reserves this grant for confidential clients, SEP-990 requires the client to authenticate, and this SDK enforces both by insisting on a shared secret. `token_endpoint_auth_method` picks where it travels: `client_secret_post` (the default, in the form body) or `client_secret_basic` (an HTTP Basic header). The profile also permits `private_key_jwt`; this provider does not support it. !!! tip Read `client_secret` from the environment or a secret manager, never from source control. @@ -68,15 +68,15 @@ The extension does not demand this; it is a deliberately stricter choice. This c The first request goes out unauthenticated, and the server's `401` starts the flow. -1. **Discovery.** It fetches the authorization server metadata from the configured issuer's RFC 8414 well-known path, checks the document's `issuer` matches, and checks the token endpoint is on the issuer's origin. +1. **Discovery.** It fetches the authorization server metadata from the configured issuer's [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) well-known path, checks the document's `issuer` matches, and checks the token endpoint is on the issuer's origin. 2. **The assertion.** It awaits your `assertion_provider`. 3. **Exchange.** It POSTs the `jwt-bearer` grant to the token endpoint, stores the `OAuthToken`, and replays your original request with `Authorization: Bearer ...`. -A `403` whose `WWW-Authenticate` names `insufficient_scope` runs steps 2 and 3 again with the union of your `scope` and the challenged one. (`scope` is only ever a request; this page's authorization server grants what the ID-JAG says and nothing else.) There is no refresh token anywhere in this: when the access token expires, the next `401` mints a fresh ID-JAG and exchanges again, and *that* is the lever the IdP holds. Failures are the same two exceptions as the rest of **OAuth clients**: `OAuthFlowError` for discovery and validation, its subclass `OAuthTokenError` when the token endpoint says no. +A `403` whose `WWW-Authenticate` names `insufficient_scope` runs steps 2 and 3 again with the union of your `scope` and the challenged one. (`scope` is only ever a request; this page's authorization server grants what the ID-JAG says and nothing else.) There is no refresh token anywhere in this: when the access token expires, the next `401` mints a fresh ID-JAG and exchanges again, and *that* is the lever the IdP holds. Failures are the same two exceptions as the rest of **[OAuth clients](oauth-clients.md)**: `OAuthFlowError` for discovery and validation, its subclass `OAuthTokenError` when the token endpoint says no. ## The authorization server -Most of the time you stop here. The MCP authorization server is somebody else's product, accepting ID-JAGs is its configuration to turn on, and the SDK's half of SEP-990 is the client above. +Most of the time you stop here. The MCP authorization server is somebody else's product, accepting ID-JAGs is its configuration to turn on, and the SDK's half of [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) is the client above. The SDK can also *be* the authorization server: `create_auth_routes` returns the authorization server's routes as a list any Starlette app can mount, which is how `examples/servers/simple-auth/` in the repository runs one. SEP-990 adds one flag and one method to that surface: @@ -93,7 +93,7 @@ The SDK can also *be* the authorization server: `create_auth_routes` returns the The SDK never decodes the assertion: only your deployment knows which IdP it trusts and which keys that IdP publishes, so everything inside `exchange_identity_assertion` is load-bearing. Verify the signature against the IdP's published keys (its JWKS; the shared secret here is the - demo's), and `iss` and `exp`, per RFC 7523 §3. Require the JWT header's `typ` to be + demo's), and `iss` and `exp`, per [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. Require the JWT header's `typ` to be `oauth-id-jag+jwt`, the profile's guard against some other JWT being replayed as a grant. Require `aud` to be your own issuer. Require the ID-JAG's `client_id` claim to equal the client the handler authenticated, and its `resource` claim to name a resource you actually serve. @@ -108,7 +108,7 @@ And notice what the returned `OAuthToken` does not carry: a refresh token. The I !!! info A server that still embeds its authorization server with `auth_server_provider=` reaches the same - code through `AuthSettings(identity_assertion_enabled=True)`. **Authorization** explains why new + code through `AuthSettings(identity_assertion_enabled=True)`. **[Authorization](authorization.md)** explains why new servers should not start there. !!! check @@ -137,10 +137,10 @@ And notice what the returned `OAuthToken` does not carry: a refresh token. The I ## Recap -* SEP-990 lets the enterprise identity provider, not the end user, decide which MCP servers a client may reach. The IdP signs that decision into an **ID-JAG**. -* Obtaining the ID-JAG is an RFC 8693 token exchange against *your IdP*, and the SDK does not make it. Presenting it to the MCP authorization server is the RFC 7523 `jwt-bearer` grant, and the SDK does both sides of that. +* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) lets the enterprise identity provider, not the end user, decide which MCP servers a client may reach. The IdP signs that decision into an **ID-JAG**. +* Obtaining the ID-JAG is an [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange against *your IdP*, and the SDK does not make it. Presenting it to the MCP authorization server is the [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) `jwt-bearer` grant, and the SDK does both sides of that. * `IdentityAssertionOAuthProvider` is another `httpx.Auth`: a pre-registered confidential client, a pinned `issuer`, and one `assertion_provider(audience, resource)` callback. No browser, no registration, no refresh token. * The authorization server is never discovered from the resource server. Configure `issuer` to exactly the string its metadata document serves; the comparison is character for character. * Server side, `identity_assertion_enabled=True` plus `exchange_identity_assertion`. The SDK authenticates the client and gates the grant; validating the ID-JAG is entirely yours, and the issued token is bound to the ID-JAG's `resource`, not the request's. -The one party this page never touched is the MCP server. What it does with the token you just minted, it was already doing in **Authorization**. +The one party this page never touched is the MCP server. What it does with the token you just minted, it was already doing in **[Authorization](authorization.md)**. diff --git a/docs/advanced/low-level-server.md b/docs/advanced/low-level-server.md index 8473495ce..2220151db 100644 --- a/docs/advanced/low-level-server.md +++ b/docs/advanced/low-level-server.md @@ -12,7 +12,7 @@ For everything else, stay on `MCPServer`. ## The same tool, by hand -This is `search_books` from **Tools** (the nine-line `@mcp.tool()` file) with the sugar removed: +This is `search_books` from **[Tools](../tutorial/tools.md)** (the nine-line `@mcp.tool()` file) with the sugar removed: ```python title="server.py" hl_lines="23 27 33" --8<-- "docs_src/lowlevel/tutorial001.py" @@ -61,7 +61,7 @@ The same text the `@mcp.tool()` version produced. Two honest differences: ## Nothing is checked for you -In **Tools** you saw a bad argument get rejected before your function ran. That was `MCPServer` validating the call against the schema it generated. +In **[Tools](../tutorial/tools.md)** you saw a bad argument get rejected before your function ran. That was `MCPServer` validating the call against the schema it generated. `Server` does not do that. Your `input_schema` is *advertised* to the client; it is never *applied* to `params.arguments`. @@ -72,9 +72,9 @@ In **Tools** you saw a bad argument get rejected before your function ran. That MCPError: Internal server error ``` - A JSON-RPC error, code `-32603`, with a deliberately generic message: the SDK won't leak your traceback to a remote caller. The model never finds out what it did wrong, so it can't retry. (In a test, `raise_exceptions=True` surfaces the real exception instead; see **Testing**.) + A JSON-RPC error, code `-32603`, with a deliberately generic message: the SDK won't leak your traceback to a remote caller. The model never finds out what it did wrong, so it can't retry. (In a test, `raise_exceptions=True` surfaces the real exception instead; see **[Testing](../tutorial/testing.md)**.) -That generalises. An exception raised from a low-level handler is **always** a protocol error, never an `is_error=True` tool result. If you want the model to read the failure and recover, validate `params.arguments` yourself and return `CallToolResult(content=[TextContent(...)], is_error=True)`. The two kinds of failure are the subject of **Handling errors**. +That generalises. An exception raised from a low-level handler is **always** a protocol error, never an `is_error=True` tool result. If you want the model to read the failure and recover, validate `params.arguments` yourself and return `CallToolResult(content=[TextContent(...)], is_error=True)`. The two kinds of failure are the subject of **[Handling errors](../tutorial/handling-errors.md)**. ## Two tools, one handler @@ -106,7 +106,7 @@ Call it and the result carries both representations: } ``` -The server never compares the two fields. This SDK's `Client` does: return `structured_content` that doesn't satisfy the `output_schema` you declared and `call_tool` raises a `RuntimeError` that starts with `Invalid structured content returned by tool search_books` and goes on to quote the `jsonschema` failure. Promising a schema is cheap; keeping it is on you. The whole ladder of return types and schemas is in **Structured Output**. +The server never compares the two fields. This SDK's `Client` does: return `structured_content` that doesn't satisfy the `output_schema` you declared and `call_tool` raises a `RuntimeError` that starts with `Invalid structured content returned by tool search_books` and goes on to quote the `jsonschema` failure. Promising a schema is cheap; keeping it is on you. The whole ladder of return types and schemas is in **[Structured Output](../tutorial/structured-output.md)**. ## `_meta`: for the application, not the model @@ -147,7 +147,7 @@ No `resources`, no `prompts`: there is nothing to back them. Pass `on_list_promp * The lifespan is a `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` on an `async` generator gives you exactly that. * Whatever it `yield`s becomes `ctx.lifespan_context`, and because the handlers are annotated `ServerRequestContext[Catalog]`, `.search(...)` autocompletes and type-checks. -* It is entered once when the server starts and exited once when it stops. Startup, teardown, and `MCPServer`'s version of the same idea are in **Lifespan**. +* It is entered once when the server starts and exited once when it stops. Startup, teardown, and `MCPServer`'s version of the same idea are in **[Lifespan](../tutorial/lifespan.md)**. Without a `lifespan=`, `ctx.lifespan_context` is an empty `dict`. @@ -175,15 +175,15 @@ use Server.middleware to observe or wrap initialization The handshake belongs to the runner. `server/discover`, `ping`, and every other built-in are yours to replace. !!! tip - `Server.middleware`, mentioned in that error, wraps **every** inbound message, including `initialize`. If what you want is to observe or rewrite traffic rather than answer a new method, start at **Middleware**. + `Server.middleware`, mentioned in that error, wraps **every** inbound message, including `initialize`. If what you want is to observe or rewrite traffic rather than answer a new method, start at **[Middleware](middleware.md)**. ## The other handlers Each of these is one idea you now have the vocabulary for; each has its own chapter. -* `on_call_tool` may return an `InputRequiredResult` instead of a `CallToolResult` to pause the call and ask the client for input; see **Multi-round-trip requests**. +* `on_call_tool` may return an `InputRequiredResult` instead of a `CallToolResult` to pause the call and ask the client for input; see **[Multi-round-trip requests](multi-round-trip.md)**. * `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` are the same `(ctx, params) -> result` shape for the other primitives. -* `server.streamable_http_app()` returns the same Starlette app `MCPServer`'s does; deploy it the way **Running your server** deploys any other ASGI app. There is no `server.run(transport=...)` down here: `server.run(read_stream, write_stream, server.create_initialization_options())` drives one connection over a pair of streams, and that one line is the whole story. +* `server.streamable_http_app()` returns the same Starlette app `MCPServer`'s does; deploy it the way **[Running your server](../run/index.md)** deploys any other ASGI app. There is no `server.run(transport=...)` down here: `server.run(read_stream, write_stream, server.create_initialization_options())` drives one connection over a pair of streams, and that one line is the whole story. ## Recap @@ -195,4 +195,4 @@ Each of these is one idea you now have the vocabulary for; each has its own chap * `add_request_handler(method, params_type, handler)` serves any method. `initialize` is reserved. * The capabilities a `Server` advertises are derived from which handlers you registered. -`Client(server)` treated both servers identically because they *are* the same protocol, which is the whole point. The next layer down isn't a class at all: it's **Middleware**. +`Client(server)` treated both servers identically because they *are* the same protocol, which is the whole point. The next layer down isn't a class at all: it's **[Middleware](middleware.md)**. diff --git a/docs/advanced/middleware.md b/docs/advanced/middleware.md index cb10c6cf1..7cc15ce3c 100644 --- a/docs/advanced/middleware.md +++ b/docs/advanced/middleware.md @@ -10,7 +10,7 @@ You write it as `async (ctx, call_next)` and append it to `server.middleware`. T Do not make it the foundation your server stands on. This is a **low-level `Server`** feature. `MCPServer` does not expose a middleware list. -If `Server(name, on_call_tool=...)` is new to you, read **The low-level Server** first. +If `Server(name, on_call_tool=...)` is new to you, read **[The low-level Server](low-level-server.md)** first. ## A timing middleware @@ -84,7 +84,7 @@ In increasing order of how much you should hesitate: The SDK ships exactly one middleware, and it is already on your server's list: the one that emits an OpenTelemetry span for every message. You don't append it, and most of the time you don't think about it. It is a no-op until you install an exporter, and it has its own page: -**OpenTelemetry**. +**[OpenTelemetry](opentelemetry.md)**. !!! info If you have written ASGI middleware, you already know this shape. Starlette's @@ -101,8 +101,8 @@ don't think about it. It is a no-op until you install an exporter, and it has it * `ctx.request_id is None` is how you tell a notification from a request. * Raise instead of calling `call_next` to refuse one message; the connection survives. * The SDK's own OpenTelemetry tracing is a middleware too, already on the list. See - **OpenTelemetry**. + **[OpenTelemetry](opentelemetry.md)**. * The whole surface is provisional. Observe with it; don't build on it. -That is everything that wraps a request. **Authorization** is what decides whether the request +That is everything that wraps a request. **[Authorization](authorization.md)** is what decides whether the request gets to run at all. diff --git a/docs/advanced/multi-round-trip.md b/docs/advanced/multi-round-trip.md index a90cb5e98..de11a8db8 100644 --- a/docs/advanced/multi-round-trip.md +++ b/docs/advanced/multi-round-trip.md @@ -29,7 +29,7 @@ The high-level `@mcp.tool()` decorator has no sugar for this yet. Today you writ * On the first call `params.input_responses` is `None`, so the guard fires and the handler asks instead of answering. * On the retry, the `ElicitResult` the client sent is sitting under the **same key** (`"region"`) that the server used in `input_requests`. -Everything else in that file (the explicit `input_schema`, the hand-built `CallToolResult`) is the ordinary low-level `Server`, covered in **The low-level Server**. This page only adds the second return type. +Everything else in that file (the explicit `input_schema`, the hand-built `CallToolResult`) is the ordinary low-level `Server`, covered in **[The low-level Server](low-level-server.md)**. This page only adds the second return type. ## The client side @@ -85,7 +85,7 @@ Drop to the underlying session, where `allow_input_required=True` hands you the **URL-mode elicitation** rides this exact mechanism on a 2026 connection. The entry in `input_requests` is an `ElicitRequest` whose params are `ElicitRequestURLParams`; the user finishes the out-of-band flow and your client retries the call. Same loop, no new API. The - high-level server half is in **Elicitation**. + high-level server half is in **[Elicitation](../tutorial/elicitation.md)**. ## Recap @@ -95,4 +95,4 @@ Drop to the underlying session, where `allow_input_required=True` hands you the * To inspect or persist rounds, use `client.session.call_tool(..., allow_input_required=True)` and own the `while isinstance(result, InputRequiredResult)` loop yourself. * The server side is the **low-level** `Server` only; `@mcp.tool()` has no sugar for this yet. -This is the mechanism that replaces server-initiated sampling and the rest of the push-style back-channel; see **Deprecated features**. +This is the mechanism that replaces server-initiated sampling and the rest of the push-style back-channel; see **[Deprecated features](deprecated.md)**. diff --git a/docs/advanced/oauth-clients.md b/docs/advanced/oauth-clients.md index 3407f0266..698a08f4f 100644 --- a/docs/advanced/oauth-clients.md +++ b/docs/advanced/oauth-clients.md @@ -4,7 +4,7 @@ Some MCP servers are protected. Send them a request without a token and they ans **`OAuthClientProvider`** is how you get the token. It is not an MCP object at all. It is an `httpx.Auth`, the standard httpx hook for "do something to every request". You attach it to an `httpx.AsyncClient`, hand that client to the Streamable HTTP transport, and stop thinking about it. -This chapter is the client side. Making your own server demand a token is **Authorization**. +This chapter is the client side. Making your own server demand a token is **[Authorization](authorization.md)**. ## The provider @@ -23,7 +23,7 @@ Nothing else in the file mentions OAuth. `main()` never sees a token. ### Client metadata -`OAuthClientMetadata` is the real RFC 7591 registration document, as a Pydantic model. +`OAuthClientMetadata` is the real [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) registration document, as a Pydantic model. You set three fields. The defaults fill in the rest: `grant_types` is already `["authorization_code", "refresh_token"]` and `response_types` is already `["code"]`, which is exactly the flow this provider runs. @@ -70,7 +70,7 @@ A real client runs a small local HTTP server on the redirect URI instead of call Look at `main()`. The provider goes on the **httpx client**, the httpx client goes into `streamable_http_client(url, http_client=...)`, and that transport goes into `Client`. -`streamable_http_client` has no `auth=` keyword. Anything HTTP-level (auth, headers, timeouts, proxies) belongs on the `httpx.AsyncClient` you bring. That layering is **Client transports**. +`streamable_http_client` has no `auth=` keyword. Anything HTTP-level (auth, headers, timeouts, proxies) belongs on the `httpx.AsyncClient` you bring. That layering is **[Client transports](../client/transports.md)**. ## What the provider does for you @@ -119,7 +119,7 @@ By default the secret travels as HTTP Basic auth on the token request (`client_s the same pattern: construct one, put it on `auth=`. The same module ships `SignedJWTParameters` and `static_assertion_provider`, two helpers that build its assertion. -There is one more no-human situation: the client belongs to an enterprise whose identity provider, not the user, decides which MCP servers it may reach. That is a different grant with its own trust model and its own chapter, **Identity assertion**. +There is one more no-human situation: the client belongs to an enterprise whose identity provider, not the user, decides which MCP servers it may reach. That is a different grant with its own trust model and its own chapter, **[Identity assertion](identity-assertion.md)**. ## When it fails @@ -136,4 +136,4 @@ Not everything is a flow error. The network can still fail; those are ordinary ` * `ClientCredentialsOAuthProvider` is the no-human version: `client_id` + `client_secret`, no handlers, no browser. * Every OAuth failure is an `OAuthFlowError`; `OAuthRegistrationError` and `OAuthTokenError` are its subclasses. -The other half of this handshake, making your *server* demand the token, is **Authorization**. +The other half of this handshake, making your *server* demand the token, is **[Authorization](authorization.md)**. diff --git a/docs/advanced/opentelemetry.md b/docs/advanced/opentelemetry.md index 0d971d6e0..80fb83f04 100644 --- a/docs/advanced/opentelemetry.md +++ b/docs/advanced/opentelemetry.md @@ -104,4 +104,4 @@ mcp._lowlevel_server.middleware[:] = [ with no change to your server. * Client-to-server trace context propagates automatically when both sides run the SDK. -Next, the thing that decides whether a request runs at all: **Authorization**. +Next, the thing that decides whether a request runs at all: **[Authorization](authorization.md)**. diff --git a/docs/advanced/pagination.md b/docs/advanced/pagination.md index ef33fa0c3..aac63f4c7 100644 --- a/docs/advanced/pagination.md +++ b/docs/advanced/pagination.md @@ -6,7 +6,7 @@ Most servers never need this. Pagination is for the server whose resource list is really a database: thousands of rows it refuses to serialize in one response. The protocol's answer is a **cursor**: the server returns a page plus an opaque token, and the client sends that token back to get the next page. -`@mcp.resource()` has no hook for any of that. To page, you write the list handler yourself, on the **low-level Server**. +`@mcp.resource()` has no hook for any of that. To page, you write the list handler yourself, on the **[low-level Server](low-level-server.md)**. ## A server that pages @@ -48,7 +48,7 @@ Every `list_*` method on `Client` (`list_tools`, `list_resources`, `list_resourc Run its `main()` and it prints `100 resources`: ten pages of ten, stitched together by a loop that never knew there were ten pages. -This is the same loop **The Client** chapter showed you, and it costs nothing against a server that doesn't page: `next_cursor` is `None` on the first response and the loop runs once. +This is the same loop **[The Client](../client/index.md)** chapter showed you, and it costs nothing against a server that doesn't page: `next_cursor` is `None` on the first response and the loop runs once. ## The three rules @@ -77,4 +77,4 @@ This is the same loop **The Client** chapter showed you, and it costs nothing ag * The client loop: pass `cursor=`, accumulate, repeat until `next_cursor is None`. * Cursors are opaque, the server owns the page size, and a non-paging client still gets page one. -The rest of the hand-written `Server` API (`on_call_tool`, `input_schema` dicts, `_meta`) is **The low-level Server**. +The rest of the hand-written `Server` API (`on_call_tool`, `input_schema` dicts, `_meta`) is **[The low-level Server](low-level-server.md)**. diff --git a/docs/advanced/session-groups.md b/docs/advanced/session-groups.md index e33004c47..952231b84 100644 --- a/docs/advanced/session-groups.md +++ b/docs/advanced/session-groups.md @@ -68,7 +68,7 @@ If you already hold a connected `ClientSession` (`Client.session` is one), hand ## The classic handshake -`ClientSessionGroup` is built on `ClientSession`, not on `Client`. Each `connect_to_server` runs the classic `initialize` handshake. It never sends the `server/discover` probe described in **Protocol versions**. Every MCP server understands that handshake, so this costs you compatibility with nothing; it only means a group takes the older, slower path to a server that could do better. +`ClientSessionGroup` is built on `ClientSession`, not on `Client`. Each `connect_to_server` runs the classic `initialize` handshake. It never sends the `server/discover` probe described in **[Protocol versions](../client/protocol-versions.md)**. Every MCP server understands that handshake, so this costs you compatibility with nothing; it only means a group takes the older, slower path to a server that could do better. ## Recap @@ -79,4 +79,4 @@ If you already hold a connected `ClientSession` (`Client.session` is one), hand * `component_name_hook=` rewrites every registered name. The dict key changes, the wire name does not. * `connect_with_session` adds a session you already hold; `disconnect_from_server` removes one. -The handshake a group speaks (and the faster one a `Client` prefers) is the subject of **Protocol versions**. +The handshake a group speaks (and the faster one a `Client` prefers) is the subject of **[Protocol versions](../client/protocol-versions.md)**. diff --git a/docs/advanced/uri-templates.md b/docs/advanced/uri-templates.md index 32560f8ec..51208d725 100644 --- a/docs/advanced/uri-templates.md +++ b/docs/advanced/uri-templates.md @@ -4,7 +4,7 @@ This is the reference for the URI-template syntax that [`@mcp.resource`](../tutorial/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**; this page assumes you're already comfortable declaring a +**[Resources](../tutorial/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. @@ -17,7 +17,7 @@ details (message formats, lifecycle, pagination) see the ## The full operator set -**Resources** showed one placeholder, `{user_id}`. There are four more +**[Resources](../tutorial/resources.md)** showed one placeholder, `{user_id}`. There are four more operator forms; here they are on one server so you can see them next to each other: @@ -201,13 +201,13 @@ These checks are a heuristic pre-filter; for filesystem access, !!! 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** for the difference between a + error response. See **[Handling errors](../tutorial/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**), you register handlers for the `resources/list` and +If you're building on the low-level `Server` (see **[The low-level +Server](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. diff --git a/docs/client/callbacks.md b/docs/client/callbacks.md index db2c4d7cd..31a4d635b 100644 --- a/docs/client/callbacks.md +++ b/docs/client/callbacks.md @@ -15,7 +15,7 @@ Here is a server whose tool can't finish on its own: * `ctx.elicit(...)` sends an `elicitation/create` request **to the client** and waits. * The tool doesn't return until somebody (a person in a form, or your code) supplies a `name`. -That is the server half, and the **Elicitation** chapter owns it. This chapter is the other end of the wire. +That is the server half, and the **[Elicitation](../tutorial/elicitation.md)** chapter owns it. This chapter is the other end of the wire. ## The elicitation callback @@ -31,7 +31,7 @@ That is the server half, and the **Elicitation** chapter owns it. This chapter i !!! tip `params` is a union of the two elicitation modes. Here `params.mode` is `"form"`; a `"url"` request carries `params.url` instead of a schema. One callback handles both; branch on `params.mode`. - **Elicitation** shows the full pattern. + **[Elicitation](../tutorial/elicitation.md)** shows the full pattern. ### Try it @@ -59,11 +59,11 @@ One `tools/call` from you, one `elicitation/create` back from the server, answer protocol path, and that path has no back-channel for server-to-client requests: `ctx.elicit` fails before your callback ever runs. The transport doesn't decide that; the negotiated protocol does, in-memory and over a URL alike. Pin `mode="legacy"` whenever your client has - to answer one; every test behind this page does. **Protocol versions** has the whole story. + to answer one; every test behind this page does. **[Protocol versions](protocol-versions.md)** has the whole story. On a 2026-07-28 session the callback isn't dead, it's fed differently: when a tool returns an `InputRequiredResult` carrying an `ElicitRequest`, `Client` dispatches that entry to the same - `elicitation_callback` and retries the call for you. That flow is **Multi-round-trip requests**. + `elicitation_callback` and retries the call for you. That flow is **[Multi-round-trip requests](../advanced/multi-round-trip.md)**. ## A callback is a capability @@ -113,7 +113,7 @@ Pass all three callbacks and you get `['elicitation', 'sampling', 'roots']`. Pas `sampling_callback` answers `sampling/createMessage`: the server asking *your* model to complete something. `list_roots_callback` answers `roots/list`: the server asking which directories it may work in. -Both work. Both follow the rule above. And both serve RPCs the **2026-07-28 spec removes**: a modern server doesn't call back into your client mid-request, it hands the request back to you as part of the tool result (**Multi-round-trip requests**). The callbacks themselves are not dead. When an `InputRequiredResult` carries a `CreateMessageRequest` or a `ListRootsRequest`, `Client`'s auto-loop dispatches it to the same `sampling_callback` or `list_roots_callback` you registered here. The whole list is in **Deprecated features**. +Both work. Both follow the rule above. And both serve RPCs the **2026-07-28 spec removes**: a modern server doesn't call back into your client mid-request, it hands the request back to you as part of the tool result (**[Multi-round-trip requests](../advanced/multi-round-trip.md)**). The callbacks themselves are not dead. When an `InputRequiredResult` carries a `CreateMessageRequest` or a `ListRootsRequest`, `Client`'s auto-loop dispatches it to the same `sampling_callback` or `list_roots_callback` you registered here. The whole list is in **[Deprecated features](../advanced/deprecated.md)**. You still need the callbacks to talk to servers that haven't moved. The signatures: @@ -131,7 +131,7 @@ Pass them to `Client(...)` exactly like `elicitation_callback`. Two more. Neither declares anything. -`logging_callback` receives every `notifications/message` a server sends, as `LoggingMessageNotificationParams` (`level`, `logger`, `data`). Protocol logging is itself deprecated by the 2026-07-28 spec (**Logging** has what to do instead), so this callback exists for the servers that still emit it. +`logging_callback` receives every `notifications/message` a server sends, as `LoggingMessageNotificationParams` (`level`, `logger`, `data`). Protocol logging is itself deprecated by the 2026-07-28 spec (**[Logging](../tutorial/logging.md)** has what to do instead), so this callback exists for the servers that still emit it. `message_handler` is the catch-all: every server notification reaches it (as well as its specific callback), and on a stream-backed transport so does every transport-level `Exception`. The one pattern worth knowing is `if isinstance(message, Exception): raise message`, so a broken connection fails loudly instead of vanishing. @@ -144,4 +144,4 @@ Two more. Neither declares anything. * `sampling_callback` and `list_roots_callback` work the same way but serve deprecated features; modern servers use multi-round-trip requests instead. * `logging_callback` and `message_handler` receive notifications. They declare nothing. -Next: the first argument you've been passing to `Client(...)` all along, **Client transports**. +Next: the first argument you've been passing to `Client(...)` all along, **[Client transports](transports.md)**. diff --git a/docs/client/index.md b/docs/client/index.md index 38efa72b6..a8026b5b9 100644 --- a/docs/client/index.md +++ b/docs/client/index.md @@ -24,7 +24,7 @@ The server at the top is only there so you have something to connect to. The cli * A URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, the production path. * A **transport**: anything you can `async with ... as (read, write)`, such as `stdio_client(...)` wrapping a subprocess. -Everything else on this page is identical across all three. Headers, subprocesses, timeouts, and the `Transport` protocol get their own chapter: **Client transports**. +Everything else on this page is identical across all three. Headers, subprocesses, timeouts, and the `Transport` protocol get their own chapter: **[Client transports](transports.md)**. ### What's on a connected client @@ -35,7 +35,7 @@ Four read-only properties, populated the moment you enter the block: * `client.protocol_version`: the protocol version the two sides agreed on. Here it is `"2026-07-28"`. * `client.instructions`: the server's `instructions=` string, or `None` if it didn't set one. -You never picked a protocol version. By default the `Client` probes the server and falls back to the classic handshake on older ones, so one client works against any era of server. When you need to control that, **Protocol versions** has the whole story. +You never picked a protocol version. By default the `Client` probes the server and falls back to the classic handshake on older ones, so one client works against any era of server. When you need to control that, **[Protocol versions](protocol-versions.md)** has the whole story. !!! tip `client.session` is the underlying `ClientSession`, the low-level escape hatch. @@ -104,7 +104,7 @@ That is why `main` narrows with `isinstance(block, TextContent)` before touching `structured_content` is the tool's return value as JSON, matching the tool's declared `output_schema`. No string parsing, no guessing. -When both are present they say the same thing twice on purpose: `content` is for a model, `structured_content` is for code. Where the structured half comes from, and how to control it, is the **Structured Output** chapter. +When both are present they say the same thing twice on purpose: `content` is for a model, `structured_content` is for code. Where the structured half comes from, and how to control it, is the **[Structured Output](../tutorial/structured-output.md)** chapter. ### `is_error`: whether the tool failed @@ -129,7 +129,7 @@ A tool that raises does **not** raise in your client. It comes back as an ordina (`call_tool("does_not_exist", {})`) and nothing raises. You get the same shape back, `is_error=True` with `Unknown tool: does_not_exist` in `content`. A `Client` method raises `MCPError` only when the server answers with a JSON-RPC **error** instead of a result, and - **Handling errors** covers when a server produces which. + **[Handling errors](../tutorial/handling-errors.md)** covers when a server produces which. ## Resources @@ -145,7 +145,7 @@ The resource verbs come in pairs: two ways to list, one way to read. `read_resource` returns `contents`, a list of `TextResourceContents` or `BlobResourceContents`. Same idea as tool content: narrow with `isinstance`, then read `.text` (or `.blob`). -A client can also **subscribe** to a resource and be told when it changes: `subscribe_resource(uri)` and `unsubscribe_resource(uri)`, same shape as everything else here. `MCPServer` doesn't implement that half. It says so up front (`server_capabilities.resources.subscribe` is `False`) and answers the request with an `MCPError`: `-32601`, *Method not found*. A server that does support subscriptions is built on the low-level `Server` (**The low-level Server**). +A client can also **subscribe** to a resource and be told when it changes: `subscribe_resource(uri)` and `unsubscribe_resource(uri)`, same shape as everything else here. `MCPServer` doesn't implement that half. It says so up front (`server_capabilities.resources.subscribe` is `False`) and answers the request with an `MCPError`: `-32601`, *Method not found*. A server that does support subscriptions is built on the low-level `Server` (**[The low-level Server](../advanced/low-level-server.md)**). ## Prompts @@ -181,7 +181,7 @@ A server with a completion handler can autocomplete prompt and resource-template * `ref` says *which* prompt or template you're filling in: a `PromptReference` or a `ResourceTemplateReference`. * `argument` is `{"name": ..., "value": ...}`: the argument and what the user has typed so far. -The answer is in `result.completion.values`. Type `"p"` and the server comes back with `['poetry']`. The server side, and how a handler uses the *other* already-filled arguments to narrow its suggestions, is the **Completions** chapter. +The answer is in `result.completion.values`. Type `"p"` and the server comes back with `['poetry']`. The server side, and how a handler uses the *other* already-filled arguments to narrow its suggestions, is the **[Completions](../tutorial/completions.md)** chapter. ## Pagination @@ -191,13 +191,13 @@ Every `list_*` method takes a `cursor=` keyword and every result carries a `next --8<-- "docs_src/client/tutorial007.py" ``` -This loop is correct against every server. `MCPServer` returns everything in one page, so `next_cursor` is `None` and the loop runs once, which is why most code never writes it. Servers that genuinely page, and the rules cursors obey, are in **Pagination**. +This loop is correct against every server. `MCPServer` returns everything in one page, so `next_cursor` is `None` and the loop runs once, which is why most code never writes it. Servers that genuinely page, and the rules cursors obey, are in **[Pagination](../advanced/pagination.md)**. ## In tests `Client(mcp)` with no process and no port is already a test harness for your server. -There is one constructor flag built for that: `Client(mcp, raise_exceptions=True)`. It only has an effect on in-memory connections, and **Testing** is the chapter that explains it and builds the whole pattern around it. +There is one constructor flag built for that: `Client(mcp, raise_exceptions=True)`. It only has an effect on in-memory connections, and **[Testing](../tutorial/testing.md)** is the chapter that explains it and builds the whole pattern around it. ## Recap @@ -209,4 +209,4 @@ There is one constructor flag built for that: `Client(mcp, raise_exceptions=True * `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt`, and `complete` round out the verbs. * Every `list_*` takes `cursor=`; loop until `next_cursor` is `None`. -Next: the things a server can ask the *client* for, and how you answer, in **Client callbacks**. +Next: the things a server can ask the *client* for, and how you answer, in **[Client callbacks](callbacks.md)**. diff --git a/docs/client/protocol-versions.md b/docs/client/protocol-versions.md index 323cc9cd4..0d4b9ab97 100644 --- a/docs/client/protocol-versions.md +++ b/docs/client/protocol-versions.md @@ -48,9 +48,9 @@ You want this for the **push-style** features. A server-initiated request is the server calling *you*: `ctx.elicit(...)` putting a form in front of your user, sampling asking your model for a completion mid-tool-call. That channel only exists on a handshake-era session. -At 2026-07-28 it is gone. The server *returns* its questions and you retry the call with the answers (**Multi-round-trip requests**). +At 2026-07-28 it is gone. The server *returns* its questions and you retry the call with the answers (**[Multi-round-trip requests](../advanced/multi-round-trip.md)**). -`mode="auto"` only gives you a handshake when the server is too old for anything else. `mode="legacy"` guarantees one. Reach for it whenever you hand `Client(...)` a `sampling_callback`, an `elicitation_callback` you want driven as a request, or a `message_handler`. **Client callbacks** goes through each. +`mode="auto"` only gives you a handshake when the server is too old for anything else. `mode="legacy"` guarantees one. Reach for it whenever you hand `Client(...)` a `sampling_callback`, an `elicitation_callback` you want driven as a request, or a `message_handler`. **[Client callbacks](callbacks.md)** goes through each. ## Pinning a version @@ -124,4 +124,4 @@ The second connection made **zero** negotiation round trips and still knows exac * A version pin (`mode="2026-07-28"`) sends no negotiation traffic at all, at the cost of a blank `server_info`. * `prior_discover=` pays that cost back: save `client.session.discover_result`, reconnect with it, get both. -A modern connection has no push channel, so how does a 2026 server ask you a question mid-call? It returns it: **Multi-round-trip requests**. +A modern connection has no push channel, so how does a 2026 server ask you a question mid-call? It returns it: **[Multi-round-trip requests](../advanced/multi-round-trip.md)**. diff --git a/docs/client/transports.md b/docs/client/transports.md index c47669267..1503979a3 100644 --- a/docs/client/transports.md +++ b/docs/client/transports.md @@ -4,7 +4,7 @@ Every `Client` talks to its server over a **transport**: the thing that actually You never configure one separately. `Client` takes a single positional argument and works the transport out from its type. -The *server* side of each (what `mcp.run()` does and what you deploy) is **Running your server**. +The *server* side of each (what `mcp.run()` does and what you deploy) is **[Running your server](../run/index.md)**. ## In memory @@ -18,7 +18,7 @@ No subprocess, no port, no bytes on a wire. The client and the server are two ob That makes it two things at once: -* **A test harness.** Every example in this documentation is exercised this way, and the **Testing** chapter builds the whole pattern around it. +* **A test harness.** Every example in this documentation is exercised this way, and the **[Testing](../tutorial/testing.md)** chapter builds the whole pattern around it. * **An embedding API.** An application that constructs the server doesn't need a network hop to call its tools. ## Streamable HTTP @@ -68,7 +68,7 @@ Two things to notice: !!! info If you know `httpx`, you already know how to do auth, proxies, event hooks, retries and connection limits here. The SDK adds nothing on top and takes nothing away. It is also where OAuth plugs in: - `httpx.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **OAuth clients**. + `httpx.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](../advanced/oauth-clients.md)**. ## stdio @@ -112,4 +112,4 @@ A **transport** is any async context manager that yields a `(read, write)` pair * A transport is anything you can `async with x as (read, write)`. `Client` hands anything that isn't a server object or a URL straight to that protocol. * Constructing a `Client` picks the transport. `async with` opens it. -Once the transport is open the two sides have to agree on a protocol version. You normally never think about it; when you do, **Protocol versions** is the page. +Once the transport is open the two sides have to agree on a protocol version. You normally never think about it; when you do, **[Protocol versions](protocol-versions.md)** is the page. diff --git a/docs/installation.md b/docs/installation.md index 13f56feec..bc2a8281c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -40,10 +40,10 @@ You don't need to know any of this to use the SDK, but if you're wondering what * [`jsonschema`](https://pypi.org/project/jsonschema/): validates a tool's structured output against its declared output schema. * [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/): OAuth token handling for authorization. * [`opentelemetry-api`](https://opentelemetry-python.readthedocs.io/): just the lightweight API, so the SDK's tracing middleware costs nothing unless you install an OpenTelemetry SDK and exporter yourself. -* [`typing-extensions`](https://typing-extensions.readthedocs.io/) and `typing-inspection`: modern typing features on Python 3.10. -* `pywin32`: Windows only, used for `stdio` subprocess management. +* [`typing-extensions`](https://typing-extensions.readthedocs.io/) and [`typing-inspection`](https://pypi.org/project/typing-inspection/): modern typing features on Python 3.10. +* [`pywin32`](https://pypi.org/project/pywin32/): Windows only, used for `stdio` subprocess management. ## Optional extras -* `mcp[cli]` adds [`typer`](https://typer.tiangolo.com/) and `python-dotenv` for the `mcp` command-line tool (`mcp dev`, `mcp run`, `mcp install`). You'll want this during development; you may not need it in a deployed server. +* `mcp[cli]` adds [`typer`](https://typer.tiangolo.com/) and [`python-dotenv`](https://pypi.org/project/python-dotenv/) for the `mcp` command-line tool (`mcp dev`, `mcp run`, `mcp install`). You'll want this during development; you may not need it in a deployed server. * `mcp[rich]` adds [`rich`](https://rich.readthedocs.io/) for nicer server logs. diff --git a/docs/migration.md b/docs/migration.md index 8c1378d11..79c15d91f 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -79,7 +79,7 @@ v1's internal client set `follow_redirects=True`; set it explicitly when supplyi ### OAuth `callback_handler` returns `AuthorizationCodeResult` -The `callback_handler` passed to `OAuthClientProvider` now returns an `AuthorizationCodeResult` instead of a `tuple[str, str | None]` of `(code, state)`. The new object adds an `iss` field so the client can validate the RFC 9207 authorization-response issuer (SEP-2468): when the redirect carries an `iss` query parameter it must match the authorization server's issuer, and a missing `iss` is rejected when the server advertised `authorization_response_iss_parameter_supported`. +The `callback_handler` passed to `OAuthClientProvider` now returns an `AuthorizationCodeResult` instead of a `tuple[str, str | None]` of `(code, state)`. The new object adds an `iss` field so the client can validate the [RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207) authorization-response issuer ([SEP-2468](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468)): when the redirect carries an `iss` query parameter it must match the authorization server's issuer, and a missing `iss` is rejected when the server advertised `authorization_response_iss_parameter_supported`. **Before (v1):** @@ -403,11 +403,11 @@ On the high-level `Client`, `call_tool`, `get_prompt`, and `read_resource` resol On `ClientSession`, `call_tool` / `get_prompt` / `read_resource` still return the bare result and raise `RuntimeError` if the server requests input. Pass `allow_input_required=True` to receive the `InputRequiredResult` instead, then drive the loop yourself with `input_responses=` / `request_state=`. `ClientSessionGroup.call_tool` accepts the same flag. -### `call_tool` mirrors `x-mcp-header` arguments into `Mcp-Param-*` headers (SEP-2243) +### `call_tool` mirrors `x-mcp-header` arguments into `Mcp-Param-*` headers ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)) For protocol 2026-07-28 over Streamable HTTP, a tool's input-schema property may carry an `x-mcp-header` annotation. When a tool the client has listed is called, each annotated argument is mirrored into an `Mcp-Param-` request header (string verbatim, integer as decimal, boolean as `true`/`false`, base64-sentinel-wrapped when not header-safe; `null`/absent arguments are omitted). The argument is also left in the request body. `list_tools` caches a tool's annotations, so list a tool before calling it to enable mirroring; a tool the client never listed emits no `Mcp-Param-*` headers. Other transports ignore the annotation. -### Server extensions API (SEP-2133) +### Server extensions API ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)) `MCPServer` now accepts opt-in extensions that bundle MCP behaviour behind a reverse-DNS identifier and advertise it under `ServerCapabilities.extensions` @@ -655,7 +655,7 @@ The underlying lookups now raise typed exceptions instead of `ValueError`. `Reso ### Resource templates: matching behavior changes -Resource template matching has been rewritten with RFC 6570 support. +Resource template matching has been rewritten with [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) support. Several behaviors have changed: **Path-safety checks applied by default.** Extracted parameter values @@ -768,7 +768,7 @@ On the high-level `Context` object (`mcp.server.mcpserver.Context`), `log()`, `. The lowlevel `ServerSession.send_log_message(data: Any)` already accepted arbitrary data and is unchanged. -`Context.log()` also now accepts all eight RFC-5424 log levels (`debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`) via the `LoggingLevel` type, not just the four it previously allowed. +`Context.log()` also now accepts all eight [RFC-5424](https://datatracker.ietf.org/doc/html/rfc5424) log levels (`debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`) via the `LoggingLevel` type, not just the four it previously allowed. ```python # Before @@ -1443,7 +1443,7 @@ Behavior changes: ### Experimental Tasks support removed -Tasks (SEP-1686) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`. The corresponding `Task*` types remain in `mcp_types` as types-only definitions. +Tasks ([SEP-1686](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686)) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`. The corresponding `Task*` types remain in `mcp_types` as types-only definitions. Tasks are expected to return as a separate MCP extension in a future release. @@ -1486,14 +1486,14 @@ On the server side, prefer the new dispatcher-agnostic `ServerSession.report_pro `url_preserve_empty_path=True` (Pydantic 2.12+). A path-less URL parsed from the wire keeps its empty path instead of acquiring a trailing slash, so e.g. an `issuer` of `https://as.example.com` round-trips as `https://as.example.com` rather than `https://as.example.com/`. This matters for -RFC 9207 / RFC 8414 issuer comparisons, which require simple string comparison (RFC 3986 §6.2.1). +[RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207) / [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) issuer comparisons, which require simple string comparison ([RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) §6.2.1). URLs constructed in Python from an already-built `AnyHttpUrl` object are unaffected (they were normalized at construction); only values parsed from strings/JSON change. This also changes the wire form of `OAuthClientMetadata.redirect_uris`: a path-less redirect URI passed as a string (e.g. `redirect_uris=['http://localhost:8080']`) now serializes as `http://localhost:8080` instead of `http://localhost:8080/`, and the client sends it verbatim in -the `/authorize` and token-exchange requests. RFC 6749 §3.1.2.3 requires authorization servers to +the `/authorize` and token-exchange requests. [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) §3.1.2.3 requires authorization servers to match redirect URIs by exact string comparison, so if you registered such a URI with a previous SDK release (with the trailing slash) and the registration is persisted in `TokenStorage`, re-register the client so the stored value matches what the SDK now transmits. @@ -1542,15 +1542,15 @@ If you relied on extra fields round-tripping through MCP types, move that data i ## New Features -### OAuth client credentials are bound to their authorization server (SEP-2352) +### OAuth client credentials are bound to their authorization server ([SEP-2352](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2352)) Persisted OAuth client credentials are now bound to the authorization server that issued them: `OAuthClientInformationFull` records an `issuer`, set by the SDK after registration. When a server's protected resource metadata later points at a different authorization server, the client discards the bound credentials (and the old tokens) and re-registers with the new server instead of presenting one server's `client_id` to another. URL-based client IDs (CIMD) are portable and unaffected; credentials with no recorded issuer (pre-registered, or stored before this change) are left as-is. No API change for existing `TokenStorage` implementations - the `issuer` round-trips through the unchanged `get_client_info`/`set_client_info`. -### Step-up authorization unions previously requested scopes (SEP-2350) +### Step-up authorization unions previously requested scopes ([SEP-2350](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350)) When a `403 insufficient_scope` challenge triggers step-up re-authorization, the OAuth client now requests the union of the previously requested scopes and the newly challenged scopes, instead of replacing the scope with only the challenged ones. This keeps permissions granted for earlier operations from being dropped when a later operation escalates. No API change; the wider scope is sent automatically on the re-authorization request. -### OAuth Dynamic Client Registration sends `application_type` (SEP-837) +### OAuth Dynamic Client Registration sends `application_type` ([SEP-837](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/837)) `OAuthClientMetadata` now carries an `application_type` field that is sent during Dynamic Client Registration. It defaults to `"native"`, which suits MCP clients that use loopback redirect URIs (CLI and desktop apps); browser-based clients served from a non-local host should set it to `"web"`: @@ -1567,7 +1567,7 @@ Under OIDC, omitting `application_type` defaults to `"web"`, which an authorizat ### Identity Assertion Authorization Grant for enterprise IdP flows (SEP-990) -The SDK now supports SEP-990's enterprise identity-provider policy controls. The client presents an Identity Assertion Authorization Grant (ID-JAG) - a signed JWT issued by the enterprise IdP - to the MCP authorization server using the RFC 7523 jwt-bearer grant (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, the ID-JAG as `assertion`), and receives an MCP access token. This matches the SEP-990 normative profile and interoperates with the other MCP SDKs. (Leg 1 - exchanging the user's IdP ID token for the ID-JAG against the IdP - is deployment-specific and out of scope for the SDK.) This is additive and opt-in on both sides; existing flows are unchanged. +The SDK now supports [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990)'s enterprise identity-provider policy controls. The client presents an Identity Assertion Authorization Grant (ID-JAG) - a signed JWT issued by the enterprise IdP - to the MCP authorization server using the [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) jwt-bearer grant (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, the ID-JAG as `assertion`), and receives an MCP access token. This matches the SEP-990 normative profile and interoperates with the other MCP SDKs. (Leg 1 - exchanging the user's IdP ID token for the ID-JAG against the IdP - is deployment-specific and out of scope for the SDK.) This is additive and opt-in on both sides; existing flows are unchanged. On the client, `IdentityAssertionOAuthProvider` (in `mcp.client.auth.extensions.identity_assertion`) is an `httpx.Auth` that posts the jwt-bearer request. The ID-JAG is supplied lazily through an async `assertion_provider(audience, resource)` callback - `audience` is the authorization server's issuer (the ID-JAG `aud`) and `resource` is the MCP server's identifier (the ID-JAG `resource` claim): @@ -1590,9 +1590,9 @@ provider = IdentityAssertionOAuthProvider( ) ``` -SEP-990 §5.1 requires the client to authenticate; this SDK currently requires a shared secret, so `client_secret` is mandatory (`token_endpoint_auth_method` chooses `client_secret_post` (default) or `client_secret_basic`; the spec also permits `private_key_jwt`). The authorization server is configuration, not discovery: `issuer` is the AS the client is provisioned for, authorization-server metadata is fetched from that issuer's RFC 8414 well-known, and the resource server is never asked which AS to use - so a hostile resource server cannot redirect the ID-JAG or secret. +SEP-990 §5.1 requires the client to authenticate; this SDK currently requires a shared secret, so `client_secret` is mandatory (`token_endpoint_auth_method` chooses `client_secret_post` (default) or `client_secret_basic`; the spec also permits `private_key_jwt`). The authorization server is configuration, not discovery: `issuer` is the AS the client is provisioned for, authorization-server metadata is fetched from that issuer's [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) well-known, and the resource server is never asked which AS to use - so a hostile resource server cannot redirect the ID-JAG or secret. -On the authorization server, set `AuthSettings(identity_assertion_enabled=True)` (or pass `identity_assertion_enabled=True` to `create_auth_routes`) and implement `exchange_identity_assertion` on your `OAuthAuthorizationServerProvider`. The method receives an `IdentityAssertionParams` (the ID-JAG `assertion`, requested scopes, and request `resource`) and returns a plain RFC 6749 `OAuthToken`. The flag gates both metadata advertisement and the token endpoint: when off, `/token` rejects the grant with `unsupported_grant_type` even if the provider implements the hook. When on, the metadata advertises the jwt-bearer grant and the `urn:ietf:params:oauth:grant-profile:id-jag` profile in `authorization_grant_profiles_supported` (the discovery mechanism per ext-auth §6). +On the authorization server, set `AuthSettings(identity_assertion_enabled=True)` (or pass `identity_assertion_enabled=True` to `create_auth_routes`) and implement `exchange_identity_assertion` on your `OAuthAuthorizationServerProvider`. The method receives an `IdentityAssertionParams` (the ID-JAG `assertion`, requested scopes, and request `resource`) and returns a plain [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) `OAuthToken`. The flag gates both metadata advertisement and the token endpoint: when off, `/token` rejects the grant with `unsupported_grant_type` even if the provider implements the hook. When on, the metadata advertises the jwt-bearer grant and the `urn:ietf:params:oauth:grant-profile:id-jag` profile in `authorization_grant_profiles_supported` (the discovery mechanism per ext-auth §6). The implementation is responsible for validating the assertion per RFC 7523 §3 and SEP-990 §5.1 - verify the signature/`iss`/`exp`/`typ`, require `aud` to be this AS, require the ID-JAG's `client_id` claim to match the authenticated client, audience-restrict the issued token to the ID-JAG's `resource` claim (not the client-controlled request `resource`), and derive scopes from the ID-JAG rather than granting the request verbatim. See `examples/snippets/servers/identity_assertion_server.py`, which fails closed. Two hardening points are enforced by the SDK: the handler rejects clients without a stored secret before calling the hook (and `ClientAuthenticator` itself now refuses a secret-based auth method registered without a secret), and Dynamic Client Registration refuses the jwt-bearer grant so the ID-JAG flow requires a pre-registered confidential client. diff --git a/docs/run/asgi.md b/docs/run/asgi.md index 2a21489a1..c72becb88 100644 --- a/docs/run/asgi.md +++ b/docs/run/asgi.md @@ -30,7 +30,7 @@ Run the app on its own (`uvicorn server:app`) and you never think about either. !!! tip `streamable_http_app()` takes the same keyword arguments as `mcp.run("streamable-http", ...)`, minus `port`: the port belongs to whatever serves the app. `host` is still accepted but binds - nothing here; the next section is what it actually controls. **Running your server** covers the + nothing here; the next section is what it actually controls. **[Running your server](index.md)** covers the options themselves. `mcp.sse_app()` does the same for the superseded SSE transport. @@ -168,4 +168,4 @@ A browser-based client needs two permissions from you: to **send** its MCP reque * Browser clients need CORS: `allow_headers` for the `Mcp-*` request headers, `expose_headers=["Mcp-Session-Id"]` for the response. * `@mcp.custom_route()` adds plain, unauthenticated HTTP endpoints next to `/mcp`. -Once the server is reachable at a real URL, **The Client** connects to it with that URL instead of a server object. +Once the server is reachable at a real URL, **[The Client](../client/index.md)** connects to it with that URL instead of a server object. diff --git a/docs/run/index.md b/docs/run/index.md index da6bb2bfd..aafb1f333 100644 --- a/docs/run/index.md +++ b/docs/run/index.md @@ -39,7 +39,7 @@ python server.py Nothing prints, and it doesn't return. It is waiting on stdin for a host to speak first. -That also means stdout **is the wire**. A stray `print()` corrupts the stream; the `logging` module writes to stderr and is the right tool. That story is in **Logging**. +That also means stdout **is the wire**. A stray `print()` corrupts the stream; the `logging` module writes to stderr and is the right tool. That story is in **[Logging](../tutorial/logging.md)**. ### Try it @@ -67,7 +67,7 @@ Each transport has its own keyword arguments, all on `run()`: * `streamable_http_path`: where the MCP endpoint lives. Default `/mcp`. * `json_response=True`: answer with plain JSON instead of an SSE stream. * `stateless_http=True`: a fresh transport per request, no session tracking. -* `event_store`, `retry_interval`, `transport_security`: resumability and DNS-rebinding protection. They can wait, until you deploy somewhere other than localhost; **ASGI** covers `transport_security`. +* `event_store`, `retry_interval`, `transport_security`: resumability and DNS-rebinding protection. They can wait, until you deploy somewhere other than localhost; **[ASGI](asgi.md)** covers `transport_security`. !!! warning Transport options go to `run()`, **not** to `MCPServer(...)`. The constructor describes what @@ -78,7 +78,7 @@ Each transport has its own keyword arguments, all on `run()`: TypeError: MCPServer.__init__() got an unexpected keyword argument 'port' ``` -`run()` is the short road. The moment you need more (your server mounted inside an existing app, two servers in one process, CORS for browser clients), you build the ASGI app yourself and hand it to any ASGI host. That is **ASGI**. +`run()` is the short road. The moment you need more (your server mounted inside an existing app, two servers in one process, CORS for browser clients), you build the ASGI app yourself and hand it to any ASGI host. That is **[ASGI](asgi.md)**. ## Server settings @@ -131,7 +131,7 @@ uv run mcp install server.py -v API_KEY=abc123 -f .env !!! tip `mcp dev` and `mcp run` only understand `MCPServer`. If you build with the low-level `Server`, - you run it yourself. See **The low-level Server**. + you run it yourself. See **[The low-level Server](../advanced/low-level-server.md)**. ## Recap @@ -143,4 +143,4 @@ uv run mcp install server.py -v API_KEY=abc123 -f .env * `mcp dev` for the Inspector, `mcp run` to execute a file, `mcp install` for Claude Desktop, `mcp version` for the version. * The transport never changes what your server *is*: all three files on this page expose the identical tool. -When `run()` itself is the limit (your server inside an app that already exists), the next step is **ASGI**. +When `run()` itself is the limit (your server inside an app that already exists), the next step is **[ASGI](asgi.md)**. diff --git a/docs/tutorial/completions.md b/docs/tutorial/completions.md index e1d1815a1..31cc8f082 100644 --- a/docs/tutorial/completions.md +++ b/docs/tutorial/completions.md @@ -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)**. diff --git a/docs/tutorial/context.md b/docs/tutorial/context.md index 17af592fb..c2d43c472 100644 --- a/docs/tutorial/context.md +++ b/docs/tutorial/context.md @@ -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)**. diff --git a/docs/tutorial/dependencies.md b/docs/tutorial/dependencies.md index 0631ccd8f..e9b4c789b 100644 --- a/docs/tutorial/dependencies.md +++ b/docs/tutorial/dependencies.md @@ -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)**. diff --git a/docs/tutorial/elicitation.md b/docs/tutorial/elicitation.md index 8a7b4c335..aa4f16820 100644 --- a/docs/tutorial/elicitation.md +++ b/docs/tutorial/elicitation.md @@ -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. diff --git a/docs/tutorial/first-steps.md b/docs/tutorial/first-steps.md index ccf1a32b5..ba59c6487 100644 --- a/docs/tutorial/first-steps.md +++ b/docs/tutorial/first-steps.md @@ -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)**. diff --git a/docs/tutorial/handling-errors.md b/docs/tutorial/handling-errors.md index 9ee6dd981..90efddc24 100644 --- a/docs/tutorial/handling-errors.md +++ b/docs/tutorial/handling-errors.md @@ -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)**. diff --git a/docs/tutorial/lifespan.md b/docs/tutorial/lifespan.md index 97ea2d096..796462f93 100644 --- a/docs/tutorial/lifespan.md +++ b/docs/tutorial/lifespan.md @@ -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)**. diff --git a/docs/tutorial/logging.md b/docs/tutorial/logging.md index 628c4ce26..bea34f1c9 100644 --- a/docs/tutorial/logging.md +++ b/docs/tutorial/logging.md @@ -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)**. diff --git a/docs/tutorial/media.md b/docs/tutorial/media.md index a473c0bba..06fde1608 100644 --- a/docs/tutorial/media.md +++ b/docs/tutorial/media.md @@ -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)**. diff --git a/docs/tutorial/progress.md b/docs/tutorial/progress.md index 3267e8919..d553de473 100644 --- a/docs/tutorial/progress.md +++ b/docs/tutorial/progress.md @@ -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. diff --git a/docs/tutorial/prompts.md b/docs/tutorial/prompts.md index 44c23fa2e..c512e96ff 100644 --- a/docs/tutorial/prompts.md +++ b/docs/tutorial/prompts.md @@ -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)**. diff --git a/docs/tutorial/resources.md b/docs/tutorial/resources.md index 749b8227d..8c63053a1 100644 --- a/docs/tutorial/resources.md +++ b/docs/tutorial/resources.md @@ -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)**. diff --git a/docs/tutorial/structured-output.md b/docs/tutorial/structured-output.md index 7f20b670a..65ab1794e 100644 --- a/docs/tutorial/structured-output.md +++ b/docs/tutorial/structured-output.md @@ -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)**. diff --git a/docs/tutorial/testing.md b/docs/tutorial/testing.md index 9e31aa095..f5fe16765 100644 --- a/docs/tutorial/testing.md +++ b/docs/tutorial/testing.md @@ -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)**. diff --git a/docs/tutorial/tools.md b/docs/tutorial/tools.md index 774638856..120b96e00 100644 --- a/docs/tutorial/tools.md +++ b/docs/tutorial/tools.md @@ -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`.