hubagenticai

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.

updated 2026-07-04 ⏱ 45–60 min

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:

PrimitiveWho triggers itUse it for
ToolThe model decidesActions and computations (“look up an order”)
ResourceThe application attaches itRead-only context (“the config file”)
PromptThe user picks itReusable 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.

newsletter

One practical agentic-AI guide in your inbox. No news, no hype.

Tutorials and decision frameworks as they ship. Unsubscribe anytime.