docs/tutorial/first-steps.md
On the landing page you wrote a server, ran it, and called a tool.
Now do it again, slowly, with all three things a server can expose, and the names for everything you just saw.
Three words you'll see on every page from here on:
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 in this chapter.
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.
--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.
Run it with the MCP Inspector:
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:
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.
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):
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())
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
That dictionary is the server's half of the handshake:
| 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 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.
Look back over this page. You wrote three small Python functions. You did not write:
a: int, b: int is the schema for add.tools/list, resources/read, prompts/get: all served for you.MCPServer made it for you.mcp dev and Client(mcp), and you never saw it.That ratio is the whole point of the SDK.
@mcp.tool(), @mcp.resource(uri), @mcp.prompt(). Name, description, and schema come from the function.{param} makes a resource template, listed separately from concrete resources.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.