Skip to content

Commit 26cf9c7

Browse files
committed
docs: make the first-steps capabilities check a real client too
The reader's first Client in the docs was still an inline Client(mcp) against the imported server object. Serve server.py over HTTP and connect by URL from a docs_src client file instead, like the rest of the docs now do, and move the in-memory mention into the pointer to Testing.
1 parent 193b512 commit 26cf9c7

2 files changed

Lines changed: 28 additions & 18 deletions

File tree

docs/get-started/first-steps.md

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Three words you'll see on every page from here on:
1212
* A **client** lives inside the host and speaks MCP. The host runs one client per server it's connected to.
1313
* A **server** is what you build with this SDK. It exposes things to clients. It never talks to the model directly.
1414

15-
You write the server. Hosts are someone else's product. The SDK also gives you a `Client`, the same class a host would use to reach a server by URL or launch it as a subprocess. On this page you'll use it in memory to inspect the server you just wrote, which is also how you'll test it.
15+
You write the server. Hosts are someone else's product. The SDK also gives you a `Client`, the same class a host would use to reach a server by URL or launch it as a subprocess. It shows up later on this page, and it is also how you'll test your servers.
1616

1717
## The three primitives
1818

@@ -78,22 +78,20 @@ You saw three tabs in the Inspector. How did it know there were three?
7878

7979
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.
8080

81-
Look at it yourself. For a quick check like this, `Client` accepts the server object directly and connects to it **in memory** (no subprocess, no port):
82-
83-
```python
84-
import asyncio
85-
86-
from mcp import Client
87-
88-
from server import mcp
81+
Look at it yourself. Leave `server.py` running over HTTP in one terminal:
8982

83+
```console
84+
uv run mcp run server.py --transport streamable-http
85+
```
9086

91-
async def main() -> None:
92-
async with Client(mcp) as client:
93-
print(client.server_capabilities.model_dump(exclude_none=True))
87+
and point a client at it from another:
9488

89+
```python title="client.py" hl_lines="7-8"
90+
--8<-- "docs_src/first_steps/tutorial001_client.py"
91+
```
9592

96-
asyncio.run(main())
93+
```console
94+
python client.py
9795
```
9896

9997
```text
@@ -113,9 +111,9 @@ That dictionary is your server's declared **capabilities**. It's the first thing
113111
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.
114112

115113
!!! info
116-
`Client(mcp)` is how you'll test your servers, and it gets a whole page: **[Testing](testing.md)**.
117-
To connect to a server that is actually running, you hand `Client` a URL or a
118-
`StdioServerParameters` instead: **[The Client](../client/index.md)**.
114+
That `client.py` is a complete MCP client, and **[The Client](../client/index.md)** is its page.
115+
In a test you skip the terminal and the port and hand `Client` the server object itself,
116+
`Client(mcp)`. That gets a whole page too: **[Testing](testing.md)**.
119117

120118
## What you did not write
121119

@@ -124,7 +122,7 @@ Look back over this page. You wrote three small Python functions. You did **not*
124122
* A JSON Schema. `a: int, b: int` *is* the schema for `add`.
125123
* A request handler. `tools/list`, `resources/read`, `prompts/get`: all served for you.
126124
* A capability declaration. `MCPServer` made it for you.
127-
* 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.
125+
* A line of protocol. The version negotiation, the JSON-RPC framing, the capability exchange: all of it happened inside `mcp dev` and `client.py`, and you never saw it.
128126

129127
That ratio is the whole point of the SDK.
130128

@@ -135,6 +133,6 @@ That ratio is the whole point of the SDK.
135133
* One decorator per primitive: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Name, description, and schema come from the function.
136134
* A URI with a `{param}` makes a resource **template**, listed separately from concrete resources.
137135
* The server's **capabilities** are declared for you, and a client only asks for what a server declares.
138-
* `Client(mcp)` connects to the server object in memory: your test harness from day one.
136+
* `Client("http://localhost:8000/mcp")` talks to your running server. Hand it the server object instead, `Client(mcp)`, and it is your test harness from day one.
139137

140138
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)**.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import anyio
2+
3+
from mcp import Client
4+
5+
6+
async def main() -> None:
7+
async with Client("http://localhost:8000/mcp") as client:
8+
print(client.server_capabilities.model_dump(exclude_none=True))
9+
10+
11+
if __name__ == "__main__":
12+
anyio.run(main)

0 commit comments

Comments
 (0)