chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# First steps
|
||||
|
||||
The **[landing page](../index.md)** moves fast: write a server, run it, call a tool.
|
||||
|
||||
This page takes it slowly, with all three things a server can expose, and a name for everything along the way.
|
||||
|
||||
## Host, client, and server
|
||||
|
||||
Three words you'll see on every page from here on:
|
||||
|
||||
* A **host** is the LLM application: Claude, an IDE, an agent runtime. It's the thing the user is talking to.
|
||||
* A **client** lives inside the host and speaks MCP. The host runs one client per server it's connected to.
|
||||
* A **server** is what you build with this SDK. It exposes things to clients. It never talks to the model directly.
|
||||
|
||||
You write the server. Hosts are someone else's product. The SDK also gives you a `Client`. You'll use it to test your servers, and it shows up later on this page.
|
||||
|
||||
## The three primitives
|
||||
|
||||
A server exposes exactly three kinds of thing. What separates them is **who decides to use them**:
|
||||
|
||||
| Primitive | Controlled by | What it is | Example |
|
||||
|---------------|-----------------|-----------------------------------------------------|------------------------------------|
|
||||
| **Tools** | The model | A function the model calls to take an action | An API call, a database write |
|
||||
| **Resources** | The application | Data the host loads into the model's context | A file's contents, an API response |
|
||||
| **Prompts** | The user | A reusable message template the user invokes by name | A slash command, a menu entry |
|
||||
|
||||
"Controlled by" is the whole point of the split. A tool runs because the **model** decided to call it. A resource is attached because the **application** decided the model needed it. A prompt runs because the **user** picked it.
|
||||
|
||||
!!! info
|
||||
If you've built a web API you already have most of the intuition: a **resource** is a `GET`
|
||||
(it loads data and changes nothing) and a **tool** is a `POST` (it does work and may have
|
||||
side effects). A **prompt** has no HTTP analogue; it's closer to a saved query the user runs
|
||||
by name.
|
||||
|
||||
## One server, all three
|
||||
|
||||
```python title="server.py" hl_lines="6 12 18"
|
||||
--8<-- "docs_src/first_steps/tutorial001.py"
|
||||
```
|
||||
|
||||
Three plain functions, three decorators. Each decorator is the entire registration:
|
||||
|
||||
* `@mcp.tool()` makes `add` a **tool**.
|
||||
* `@mcp.resource("greeting://{name}")` makes `greeting` a **resource template**: the `{name}` in the URI is the function's parameter.
|
||||
* `@mcp.prompt()` makes `summarize` a **prompt**. The string it returns becomes a user message.
|
||||
|
||||
Everything else (the name, the description, the argument schema) the SDK reads from the function itself: its name, its docstring, its type hints. You never declared any of it separately.
|
||||
|
||||
!!! tip
|
||||
The two halves of the SDK have two import paths: `from mcp import Client` and
|
||||
`from mcp.server import MCPServer`. There is no `from mcp import MCPServer`.
|
||||
|
||||
### Try it
|
||||
|
||||
Run it with the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Open the URL it prints. The Inspector has one tab per primitive; walk through them in order.
|
||||
|
||||
**Tools.** One entry: `add`, described as *Add two numbers.* The form has a required integer field for `a` and another for `b`. Fill them in, call it, and the result is `3`. The Inspector built that form from `a: int, b: int`. So does every other client.
|
||||
|
||||
**Resources.** The *Resources* list is empty. `greeting` is under **Resource Templates**, because `greeting://{name}` has a parameter: there is no single resource to list until someone supplies a `name`. Give it `World` and read it:
|
||||
|
||||
```text
|
||||
Hello, World!
|
||||
```
|
||||
|
||||
**Prompts.** One entry: `summarize`, with a single required `text` argument. Get it with some text and you receive one message with `role: user` and your rendered string as the content. That's all a prompt is: a function that builds messages.
|
||||
|
||||
The Inspector ran your server over **stdio**, one of the transports an MCP server can speak. You don't pick one yet; **[Running your server](../run/index.md)** is the page for that.
|
||||
|
||||
## Capabilities
|
||||
|
||||
You saw three tabs in the Inspector. How did it know there were three?
|
||||
|
||||
When a client connects, the server declares its **capabilities**: which families of requests it will answer. The client uses that declaration to decide what to even ask for. You never wrote it; `MCPServer` declares it for you.
|
||||
|
||||
Look at it yourself. The SDK's `Client` accepts the server object directly and connects to it **in memory** (no subprocess, no port):
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
|
||||
```
|
||||
|
||||
That dictionary is your server's declared **capabilities**. It's the first thing every connecting client learns:
|
||||
|
||||
| Capability | The client may now call |
|
||||
|-------------|------------------------------------------------------------|
|
||||
| `tools` | `tools/list`, `tools/call` |
|
||||
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
|
||||
| `prompts` | `prompts/list`, `prompts/get` |
|
||||
|
||||
`MCPServer` serves all three primitives, so all three are always declared.
|
||||
|
||||
Notice what isn't there. `completions` (argument autocomplete for resource templates and prompts) needs a handler you write, this server doesn't have one, so the capability is absent and a well-behaved client won't ask. That's the rule for everything optional: register the thing and the capability appears; **[Completions](../servers/completions.md)** proves it.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` is the same in-memory client every example in these docs is tested with, and
|
||||
it's how you'll test yours. It gets a whole page: **[Testing](testing.md)**.
|
||||
|
||||
## What you did not write
|
||||
|
||||
Look back over this page. You wrote three small Python functions. You did **not** write:
|
||||
|
||||
* A JSON Schema. `a: int, b: int` *is* the schema for `add`.
|
||||
* A request handler. `tools/list`, `resources/read`, `prompts/get`: all served for you.
|
||||
* A capability declaration. `MCPServer` made it for you.
|
||||
* A line of protocol. The version negotiation, the JSON-RPC framing, the capability exchange: all of it happened inside `mcp dev` and `Client(mcp)`, and you never saw it.
|
||||
|
||||
That ratio is the whole point of the SDK.
|
||||
|
||||
## Recap
|
||||
|
||||
* A **host** is the LLM app, a **client** is its MCP-speaking half, a **server** is what you build.
|
||||
* Tools are **model**-controlled, resources are **application**-controlled, prompts are **user**-controlled.
|
||||
* One decorator per primitive: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Name, description, and schema come from the function.
|
||||
* A URI with a `{param}` makes a resource **template**, listed separately from concrete resources.
|
||||
* The server's **capabilities** are declared for you, and a client only asks for what a server declares.
|
||||
* `Client(mcp)` connects to the server object in memory: your test harness from day one.
|
||||
|
||||
Next up is **[Connect to a real host](real-host.md)**: this server inside Claude Desktop or an IDE, for real. Then **[Testing](testing.md)**: one page, one in-memory client, and you're never guessing whether it works. After that, each primitive gets its own page, starting with the one the model drives: **[Tools](../servers/tools.md)**.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Get started
|
||||
|
||||
New to MCP, or new to this SDK? Start here. These pages take you from nothing to a
|
||||
working, tested server: [install the SDK](installation.md), build your
|
||||
[first server](first-steps.md), [connect it to a real host](real-host.md), and
|
||||
[test it](testing.md) with an in-memory client.
|
||||
|
||||
## Run the code
|
||||
|
||||
All the code blocks can be copied and used directly: they are complete, working files.
|
||||
|
||||
To follow along, paste a block into a `server.py` and open it in the MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
It is **HIGHLY encouraged** that you write (or copy) the code, edit it, and run it locally. Using it in your own editor is what really shows you the point: how little you write, the autocompletion, the type checks catching mistakes before you run anything.
|
||||
|
||||
## You will not be guessing
|
||||
|
||||
Every example in these docs is a complete file under [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) in the SDK's own repository, and every one of them is exercised by the SDK's test suite through an **in-memory client**:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result.structured_content == {"result": 3}
|
||||
```
|
||||
|
||||
No subprocess, no port, no transport. `Client(mcp)` connects to the server object directly.
|
||||
|
||||
If a change to the SDK breaks an example on one of these pages, CI goes red before the page does. The code you read here is the code that runs.
|
||||
|
||||
You'll use this yourself in [Testing](testing.md); it's how you test your own servers, too.
|
||||
|
||||
## Where to go next
|
||||
|
||||
Once you have a server running, the rest of these docs are a reference, not a course.
|
||||
Every page stands on its own, so jump straight to what you need:
|
||||
|
||||
* What a server exposes (tools, resources, prompts) is **[Servers](../servers/index.md)**.
|
||||
* What's available inside the functions you register is **[Inside your handler](../handlers/index.md)**.
|
||||
* Getting it in front of clients (stdio, HTTP, your existing FastAPI app) is **[Running your server](../run/index.md)**.
|
||||
* Building the other side, an application that *uses* MCP servers, is **[Clients](../client/index.md)**.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Installation
|
||||
|
||||
The Python SDK is on PyPI as [`mcp`](https://pypi.org/project/mcp/). It requires **Python 3.10+**.
|
||||
|
||||
These docs describe **v2**, which is in beta, so the version pin is not optional yet:
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add "mcp[cli]==2.0.0b1"
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install "mcp[cli]==2.0.0b1"
|
||||
```
|
||||
|
||||
!!! warning "Why the pin"
|
||||
Installers never select a pre-release unless you name one, so an unpinned `uv add "mcp[cli]"`
|
||||
gives you the latest **v1.x** release, which these docs do not describe. Check the
|
||||
[release history](https://pypi.org/project/mcp/#history) for the newest beta before you copy
|
||||
the line above.
|
||||
|
||||
The same applies to one-off commands: `uv run --with "mcp==2.0.0b1" ...`, not `uv run --with mcp ...`.
|
||||
|
||||
If your *package* depends on `mcp`, add a `<2` upper bound (for example `mcp>=1.27,<2`) before
|
||||
the stable v2 lands so the major version bump doesn't surprise you.
|
||||
|
||||
## What gets installed
|
||||
|
||||
You don't need to know any of this to use the SDK, but if you're wondering what each dependency is for:
|
||||
|
||||
* `mcp-types`: every protocol type (requests, results, content blocks) as its own package, versioned in lockstep with the SDK. Every `from mcp_types import ...` in these docs is this package.
|
||||
* [`anyio`](https://anyio.readthedocs.io/): the async runtime. The whole SDK is written against anyio, so it runs on either `asyncio` or `trio`.
|
||||
* [`pydantic`](https://docs.pydantic.dev/): what every `mcp_types` model is built on, plus all schema generation and validation.
|
||||
* [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/): server configuration via `MCP_*` environment variables and `.env` files.
|
||||
* [`httpx`](https://www.python-httpx.org/) and [`httpx-sse`](https://pypi.org/project/httpx-sse/): the HTTP client behind the Streamable HTTP and SSE *client* transports.
|
||||
* [`starlette`](https://www.starlette.io/), [`uvicorn`](https://www.uvicorn.org/), [`sse-starlette`](https://pypi.org/project/sse-starlette/), and [`python-multipart`](https://pypi.org/project/python-multipart/): the HTTP *server* transports.
|
||||
* [`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`](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`](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.
|
||||
@@ -0,0 +1,182 @@
|
||||
# Connect to a real host
|
||||
|
||||
A **host** is the application your server ends up inside: Claude Desktop, Claude Code, an IDE. The host is what the user talks to. Inside it, an MCP **client** launches your server as a child process and speaks to it over that process's stdin and stdout.
|
||||
|
||||
Which means connecting to a host is one act: you tell it **the command that starts your server**. Everything on this page (two CLI commands, three JSON files) is a different place to put that same command.
|
||||
|
||||
## One server, every host
|
||||
|
||||
```python title="server.py" hl_lines="3 33-34"
|
||||
--8<-- "docs_src/real_host/tutorial001.py"
|
||||
```
|
||||
|
||||
Two tools and a resource, one file. Three things about that file matter to every host below:
|
||||
|
||||
* `mcp.run()` with no arguments starts a **stdio** server: it blocks, reads protocol messages on stdin, and writes them on stdout. That is the transport every host on this page speaks. The host starts your file as a child process and owns those two pipes, which is why connecting is only ever "here is the command". You never pick a port, and nothing listens on one.
|
||||
* `run()` is under `if __name__ == "__main__":`. Everything below **imports** this file rather than executing it, so an unguarded `run()` would start a server the moment anything loaded the module.
|
||||
* The server object is a module-level global named `mcp`. That's the name `mcp run` looks for (`server` and `app` also work). Call it something else and you name it explicitly: `mcp run server.py:bookshop`.
|
||||
|
||||
That is the last line of Python on this page. From here down it is all host configuration.
|
||||
|
||||
## The launch command
|
||||
|
||||
Every host below gets the same command:
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]==2.0.0b1" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
One command for all of them because `uv run --with` resolves the pinned SDK into a fresh environment on the spot: it works from any directory, needs no project and no virtual environment to activate, and always gets the exact `mcp` version these docs describe. That matters here more than anywhere else, because a host launches your server from *its* working directory with a near-empty environment, not from your shell.
|
||||
|
||||
It is also the command `mcp install` writes into Claude Desktop's config for you (below), so what you type by hand and what the tool generates agree.
|
||||
|
||||
!!! warning "The version pin is not optional"
|
||||
v2 of this SDK is in beta, and installers never select a pre-release unless you name one. An
|
||||
unpinned `--with "mcp[cli]"` gives you the latest **v1.x**, which these docs do not describe.
|
||||
Use the exact pin from **[Installation](installation.md)**.
|
||||
|
||||
!!! tip "If a host can't find `uv`"
|
||||
A host spawns your server with a minimal `PATH`, and `uv` may not be on it. Replace the bare
|
||||
`uv` with the absolute path from `which uv` (macOS/Linux) or `where uv` (Windows). That is
|
||||
exactly what `mcp install` writes.
|
||||
|
||||
!!! note "This page is the local story"
|
||||
Everything here runs your server on the machine the host is on: the host launches your
|
||||
file, over stdio. That is exactly right for a personal or single-machine tool. To give a
|
||||
server to people who do *not* have your file, you hand out a **URL**, not a command: the
|
||||
same `mcp` object served over Streamable HTTP. **[Running your server](../run/index.md)**
|
||||
is that decision in one table, and **[Deploy & scale](../run/deploy.md)** is the road from
|
||||
there to a real hostname.
|
||||
|
||||
And a host is nothing more than an application with an MCP client inside it, so your own
|
||||
Python can play the host's part: **[Client transports](../client/transports.md)** launches
|
||||
this same file as a subprocess with `stdio_client(...)`, and **[Testing](testing.md)**
|
||||
connects to it in memory with no process at all.
|
||||
|
||||
## Claude Desktop
|
||||
|
||||
The one host the SDK can configure for you:
|
||||
|
||||
```bash
|
||||
uv run mcp install server.py
|
||||
```
|
||||
|
||||
That's it. `mcp install` imports the file to read the server's name, finds Claude Desktop's config file, and writes the launch command into it. Along the way it converts your path to an absolute one, so you don't have to.
|
||||
|
||||
There is nothing to be mystified by. This is the entry it writes:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"Bookshop": {
|
||||
"command": "/absolute/path/to/uv",
|
||||
"args": [
|
||||
"run",
|
||||
"--frozen",
|
||||
"--with",
|
||||
"mcp[cli]==2.0.0b1",
|
||||
"mcp",
|
||||
"run",
|
||||
"/absolute/path/to/server.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That's the launch command from the section above with two additions: the absolute path to `uv`, and `--frozen` so `uv` never rewrites a lockfile it happens to be near. It lands in `claude_desktop_config.json`, which lives at:
|
||||
|
||||
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
You can write that file by hand. `mcp install` exists so you don't make the two classic mistakes (a relative path, a missing version pin) while doing it.
|
||||
|
||||
Fully quit Claude Desktop (not just its window) and reopen it.
|
||||
|
||||
!!! warning
|
||||
`mcp install` fails with `Claude app not found` if Claude Desktop's config *directory* doesn't
|
||||
exist yet. Install Claude Desktop and run it once: that's what creates the directory.
|
||||
|
||||
!!! tip
|
||||
Claude Desktop starts your server in its own process, so your shell's environment variables are
|
||||
not there. `uv run mcp install server.py -v API_KEY=abc123` (or `-f .env`) records them in the
|
||||
entry's `env` field. `--name` overrides the entry name; it defaults to the server's `name`.
|
||||
|
||||
## Claude Code
|
||||
|
||||
There is no file to edit. Register the server with the `claude` CLI; everything after `--` is the launch command.
|
||||
|
||||
```bash
|
||||
claude mcp add bookshop -- uv run --with "mcp[cli]==2.0.0b1" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Run `/mcp` inside a Claude Code session to confirm `bookshop` is connected and its tools are listed.
|
||||
|
||||
## Cursor
|
||||
|
||||
Create `.cursor/mcp.json` in your project root.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"bookshop": {
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]==2.0.0b1", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The same `command` plus `args`, under the same `mcpServers` key Claude Desktop uses. The server appears in Cursor's MCP settings with both tools listed.
|
||||
|
||||
## VS Code
|
||||
|
||||
Create `.vscode/mcp.json` in your project root.
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"bookshop": {
|
||||
"type": "stdio",
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]==2.0.0b1", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Two differences from Cursor's file, and they are the only two: the wrapper key is `servers`, not `mcpServers`, and each entry declares its `type`. Confirm the trust prompt, then **MCP: List Servers** in the Command Palette shows `bookshop` running.
|
||||
|
||||
!!! note
|
||||
You need VS Code 1.99 or later with the **GitHub Copilot** extension signed in (Copilot Free is
|
||||
enough), and Copilot Chat must be in **Agent** mode, because no other mode calls tools.
|
||||
|
||||
## It doesn't show up
|
||||
|
||||
Before you touch any host config, run the launch command yourself:
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]==2.0.0b1" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Nothing prints, and it doesn't return. That silence is correct: a stdio server is waiting for a host to speak first on stdin (`Ctrl-C` to stop it). A traceback or an immediate exit is the real bug, and now you can read it instead of guessing at it through a host.
|
||||
|
||||
Once that command sits and waits, what's left is almost always one of three things:
|
||||
|
||||
* **A relative path.** The host launches your server from *its* working directory, not the one you registered from. `server.py` where `/absolute/path/to/server.py` is needed is the single most common failure. If the host can't find `uv` either, that path has to be absolute too.
|
||||
* **The host is still running its old config.** Hosts read their config at launch. Claude Desktop in particular has to be *fully quit* (not just its window closed) and reopened before an edit to `claude_desktop_config.json` takes effect.
|
||||
* **Something reached stdout.** On stdio, stdout *is* the protocol. One stray `print()` and the host reads a corrupt message and drops the connection. Log with the `logging` module, which writes to stderr. **[Logging](../handlers/logging.md)** has the whole story.
|
||||
|
||||
Claude Desktop keeps a log per server: `mcp-server-<NAME>.log` is your server's stderr, next to `mcp.log` for connections, under `~/Library/Logs/Claude` on macOS and `%APPDATA%\Claude\logs` on Windows.
|
||||
|
||||
For anything past those three, **[Troubleshooting](../troubleshooting.md)** is the page.
|
||||
|
||||
## Recap
|
||||
|
||||
* A **host** (Claude Desktop, an IDE) runs an MCP client that launches your server as a child process over stdio. Connecting means giving it one launch command.
|
||||
* That command is `uv run --with "mcp[cli]==2.0.0b1" mcp run /absolute/path/to/server.py`: version-pinned, no venv to activate, works from any directory. The pin is mandatory while v2 is in beta.
|
||||
* **Claude Desktop** is the one host `mcp install` configures for you. It writes that same command (plus the absolute path to `uv`) into `claude_desktop_config.json`, so you never have to.
|
||||
* **Claude Code** is `claude mcp add bookshop -- <launch command>`. **Cursor** is `.cursor/mcp.json` under `mcpServers`. **VS Code** is `.vscode/mcp.json` under `servers`, each entry with a `type`.
|
||||
* Absolute paths everywhere, restart the host after editing its config, and never let anything but the SDK write to stdout.
|
||||
|
||||
Every host on this page connected to the same file, with the same command. What that file can *expose* is the rest of these docs: **[Tools](../servers/tools.md)**, **[Resources](../servers/resources.md)**, and every transport besides stdio in **[Running your server](../run/index.md)**.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Testing
|
||||
|
||||
The Python SDK ships a `Client` class with an **in-memory transport**: pass it your server object and it connects to it directly.
|
||||
|
||||
No subprocess. No port. No transport at all. It's the same idea as FastAPI's `TestClient`.
|
||||
|
||||
## Basic usage
|
||||
|
||||
Let's assume you have a simple server with a single tool:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/testing/tutorial001.py"
|
||||
```
|
||||
|
||||
To run the test below you'll need two extra (development) dependencies:
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add --dev pytest inline-snapshot
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install pytest inline-snapshot
|
||||
```
|
||||
|
||||
!!! info
|
||||
These docs assume you already know [`pytest`](https://docs.pytest.org/en/stable/).
|
||||
|
||||
[`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) is what the test below
|
||||
uses to assert on the whole result object in one line. It records the output of a test as the
|
||||
`snapshot(...)` literal you see. If you'd rather not use it, drop the import and assert on the
|
||||
fields you care about (`result.content[0].text == "3"`) like in any other test.
|
||||
|
||||
Now the test:
|
||||
|
||||
```python title="test_server.py"
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp import Client
|
||||
from mcp_types import CallToolResult, TextContent
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend(): # (1)!
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(): # (2)!
|
||||
async with Client(mcp, raise_exceptions=True) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_add_tool(client: Client):
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result == snapshot(
|
||||
CallToolResult(
|
||||
content=[TextContent(type="text", text="3")],
|
||||
structured_content={"result": 3},
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
1. If you are using `trio`, return `"trio"` instead. See the [anyio documentation](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on) for the details.
|
||||
2. The fixture yields a connected client. Every test that takes `client` gets a fresh in-memory connection to the same server.
|
||||
|
||||
There you go! You can now extend your tests to cover more scenarios.
|
||||
|
||||
## Why `raise_exceptions=True`?
|
||||
|
||||
Two different things can go wrong, and this flag only touches one of them.
|
||||
|
||||
An exception inside one of **your tools** is not a protocol failure. It becomes a normal result with
|
||||
`is_error=True`, and the model reads the message. `raise_exceptions` doesn't change that: with or
|
||||
without it, `call_tool` returns the same `is_error=True` result. There's a whole page on it:
|
||||
**[Handling errors](../servers/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
|
||||
never leak the details of an unexpected crash to a remote caller. In a test that is exactly what
|
||||
you *don't* want, and it is what `raise_exceptions=True` changes: your test sees the real message
|
||||
instead of the sanitised one.
|
||||
|
||||
Leave it on in tests. It has no meaning in production code.
|
||||
|
||||
## In-process by default
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` connects in-process and is **era-neutral** by default: it probes the server and
|
||||
picks the appropriate protocol path. Pin `mode="legacy"` if your test exercises legacy-specific
|
||||
semantics (sampling or elicitation push, `message_handler`), and drop `raise_exceptions=True`
|
||||
there: a legacy connection never sanitises in the first place, and the flag re-raises the
|
||||
failure inside the server task instead of in your test.
|
||||
|
||||
That one line is also why these docs can promise you that their examples work: every
|
||||
example file is exercised by the SDK's own test suite, almost all of them through exactly this
|
||||
client. You're using the same tool the SDK uses on itself.
|
||||
|
||||
You have a working, tested server. Putting it inside a real application (Claude Desktop, an
|
||||
IDE) is **[Connect to a real host](real-host.md)**; every other way to serve it is
|
||||
**[Running your server](../run/index.md)**.
|
||||
Reference in New Issue
Block a user