Tutorials Beginner
How to build your first MCP server, step by step (Python and TypeScript)
Build a working Model Context Protocol server with tools and resources, connect it to a real client, and understand what actually happens on the wire.
The Model Context Protocol (MCP) is the USB port of agentic AI: one connector standard that lets any model plug into your tools, files, and data. This tutorial builds the smallest MCP server worth building — one tool, one resource — and, more importantly, shows you how to verify it works before you point a model at it.
Step 1 — Understand the three primitives
Before writing code, know what you’re exposing. An MCP server offers three things to a client:
| Primitive | Who triggers it | Use it for |
|---|---|---|
| Tool | The model decides | Actions and computations (“look up an order”) |
| Resource | The application attaches it | Read-only context (“the config file”) |
| Prompt | The user picks it | Reusable interaction templates |
Most first servers only need tools. We’ll add a resource so you see both.
Step 2 — Python track: scaffold with the official SDK
mkdir hello-mcp && cd hello-mcp
python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]"
Create server.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hello-mcp")
@mcp.tool()
def order_status(order_id: str) -> str:
"""Look up the shipping status of an order by its ID."""
# Stand-in for a real database call.
fake_db = {"1001": "shipped", "1002": "processing"}
return fake_db.get(order_id, "not found")
@mcp.resource("config://policies/returns")
def returns_policy() -> str:
"""The current returns policy document."""
return "Returns accepted within 30 days with receipt."
if __name__ == "__main__":
mcp.run() # stdio transport by default
That’s a complete server. The docstrings matter: they become the tool descriptions the model reads when deciding what to call.
Step 3 — Verify with the MCP Inspector
Never debug a server through a model — use the Inspector first:
mcp dev server.py
This opens a browser UI where you can list tools, call order_status
with {"order_id": "1001"}, and see the raw JSON-RPC request and response.
If it works here, it will work in any client.
Step 4 — TypeScript track: the same server in Node
mkdir hello-mcp-ts && cd hello-mcp-ts
npm init -y && npm install @modelcontextprotocol/sdk zod
Create server.mjs:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'hello-mcp', version: '1.0.0' });
server.tool(
'order_status',
'Look up the shipping status of an order by its ID.',
{ order_id: z.string() },
async ({ order_id }) => {
const fakeDb = { 1001: 'shipped', 1002: 'processing' };
return { content: [{ type: 'text', text: fakeDb[order_id] ?? 'not found' }] };
}
);
server.resource(
'returns-policy',
'config://policies/returns',
async (uri) => ({
contents: [{ uri: uri.href, text: 'Returns accepted within 30 days with receipt.' }],
})
);
await server.connect(new StdioServerTransport());
Verify it the same way: npx @modelcontextprotocol/inspector node server.mjs.
Step 5 — Connect it to a real client
For Claude Code, register the server from your project directory:
claude mcp add hello-mcp -- python /full/path/to/server.py
Then ask: “What’s the status of order 1001?” — the model will discover
order_status, call it, and answer “shipped.” You built the bridge; the
model found it on its own. That discovery step is the entire point of MCP.
Troubleshooting
Client shows the server as “failed” immediately
Nine times out of ten the command path is wrong. MCP clients launch your
server as a subprocess; use absolute paths (/home/you/hello-mcp/.venv/bin/python)
rather than relying on the client inheriting your shell’s virtualenv.
mcp: command not found after pip install
You installed mcp without the CLI extra, or your venv isn’t activated.
Run pip install "mcp[cli]" inside the activated venv.
Tools appear but every call returns an error
Print to stderr, never stdout. On stdio transport, stdout carries the
JSON-RPC protocol — a stray print() corrupts the stream. Use
logging (Python) or console.error (Node) for debugging output.
Was this guide useful?
Thanks — noted. It shapes what gets written next.
newsletter
One practical agentic-AI guide in your inbox. No news, no hype.
Tutorials and decision frameworks as they ship. Unsubscribe anytime.