tutorial

MCP Tutorial: Build Your First MCP Server Step-by-Step

Complete MCP tutorial with Python and TypeScript. Build, test, and deploy your first MCP server in 30 minutes. Includes real-world patterns and production tips.

MCPize Team
MCPize TeamCore Team
November 16, 20259 min read
MCP Tutorial hero showing Python and TypeScript code connecting to AI tools

MCP Tutorial: Build Your First MCP Server from Scratch

TL;DR: Use Python + FastMCP for the fastest path. Define tools with @mcp.tool decorators, test with MCP Inspector before Claude, and never print to stdout. Working server in ~25 lines of code.

I remember my first MCP server. It took me three hours to get "Hello World" working — not because MCP is hard, but because I made every mistake in the book. Wrong paths in config. Stdout pollution breaking JSON-RPC. Authentication that worked locally but failed everywhere else.

This MCP tutorial exists so you don't repeat those mistakes. In 30 minutes, you'll build a working server, test it properly, and understand what actually matters for production.

What we're building: A note-taking MCP server. Simple enough to understand, complex enough to demonstrate real patterns.

What you'll learn:

  • The three MCP primitives (tools, resources, prompts) and when to use each
  • Python vs TypeScript — which to choose and why
  • Testing strategies that catch bugs before Claude does
  • The security mistakes 53% of MCP developers make
  • Production deployment patterns used by real servers
New to MCP? Start with the basics

The MCP Ecosystem in 2025: What You're Building Into#

Before writing code, understand the landscape. MCP has grown explosively since Anthropic released it in November 2024:

MCP Server Growth
MetricValue (Dec 2025)
Total MCP servers6,800+
Weekly npm downloads3.4 million
GitHub stars (mcp repo)66,000+
Companies using MCPOpenAI, Google, Microsoft, Anthropic

This isn't a toy protocol. The Linux Foundation now governs MCP as an open standard. OpenAI adopted it for ChatGPT desktop. Microsoft integrated it into VS Code Copilot. When you build an MCP server, you're building for an ecosystem that's becoming infrastructure.

MCP Architecture: What Actually Matters#

Skip the diagrams. Here's what you need to know:

Three primitives. That's it.

PrimitivePurposeYour Example
ToolsActions AI can takeadd_note, search_notes
ResourcesData AI can readnotes://meeting-notes
PromptsReusable instructionsNot covered here

Tools are 90% of what you'll build. When someone asks Claude "save this as a note," Claude calls your add_note tool. When they ask "what notes do I have?", Claude calls list_notes. Simple request-response.

Resources are for read-only data. Think files, database records, API responses. Claude can browse them but can't modify through resources — that's what tools are for.

The communication is JSON-RPC over stdio. Claude Desktop spawns your server as a subprocess. They exchange JSON messages through stdin/stdout. Your server never needs to handle HTTP, WebSockets, or authentication for local use.

Python or TypeScript? The Real Tradeoffs#

Both work. Here's how to decide:

FactorPython (FastMCP)TypeScript (SDK)
Setup time2 minutes4 minutes
Lines of code~40% lessMore verbose
Type safetyRuntime onlyCompile-time
Best forPrototypes, data science, MLProduction apps, type-heavy codebases
Async supportNative async/awaitNative async/await

My recommendation: Start with Python/FastMCP for this tutorial. The decorator syntax eliminates boilerplate. You can always rewrite in TypeScript later if you need stricter typing.

Python Tutorial: Build the Notes Server#

Setup (2 minutes)#

mkdir mcp-notes && cd mcp-notes
python -m venv venv && source venv/bin/activate
pip install fastmcp

The Core Pattern#

FastMCP uses decorators. One decorator = one MCP tool:

from fastmcp import FastMCP

mcp = FastMCP(name="NotesServer")
notes: dict[str, str] = {}

@mcp.tool
def add_note(title: str, content: str) -> str:
    """Add a new note."""  # This docstring becomes the tool description
    notes[title] = content
    return f"Added: {title}"

That's a working MCP server. The @mcp.tool decorator:

  • Registers the function as an MCP tool
  • Generates JSON Schema from type hints
  • Uses the docstring as the tool description Claude sees

Adding More Tools#

@mcp.tool
def list_notes() -> str:
    """List all note titles."""
    return "\n".join(notes.keys()) if notes else "No notes yet"

@mcp.tool
def search_notes(query: str) -> str:
    """Search notes by keyword."""
    matches = [t for t, c in notes.items() if query.lower() in c.lower()]
    return "\n".join(matches) if matches else "No matches"

Running It#

if __name__ == "__main__":
    mcp.run(transport="stdio")

Complete server: ~25 lines. That's the power of FastMCP.

TypeScript Tutorial: The Same Server#

For those who prefer TypeScript or need it for their stack:

mkdir mcp-notes-ts && cd mcp-notes-ts
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node

The TypeScript SDK is more explicit. You define tool schemas manually:

const notes = new Map<string, string>();

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "add_note",
    description: "Add a new note",
    inputSchema: {
      type: "object",
      properties: {
        title: { type: "string" },
        content: { type: "string" }
      },
      required: ["title", "content"]
    }
  }]
}));

Then handle the calls:

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  if (name === "add_note") {
    notes.set(args.title, args.content);
    return { content: [{ type: "text", text: `Added: ${args.title}` }] };
  }
});

More verbose, but you get compile-time type checking. Worth it for larger servers.

Testing: Where Most Tutorials Fail You#

Here's what nobody tells you: the MCP Inspector is non-negotiable. Don't skip to Claude Desktop testing. You'll waste hours on bugs that the Inspector catches in seconds.

MCP Inspector (Required First Step)#

npx @anthropic/mcp-inspector python server.py

This opens a browser UI where you can:

  • See all registered tools and their schemas
  • Call tools with test inputs
  • View the raw JSON-RPC messages
  • Catch schema errors before Claude does

Testing workflow:

  1. Add tool → Test in Inspector → Works? → Next tool
  2. Never batch multiple changes before testing
  3. If it works in Inspector but fails in Claude, the bug is in your config

Manual JSON-RPC Testing#

For quick checks without the Inspector:

echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | python server.py

You should see your tools in the response. If you see Python errors or empty results, fix those first.

Connect to Claude Desktop#

Now the payoff. Find your config file:

OSPath
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
Linux~/.config/Claude/claude_desktop_config.json

Add your server:

{
  "mcpServers": {
    "notes": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"]
    }
  }
}

Critical: Use absolute paths. Relative paths fail silently.

Restart Claude Desktop (fully quit, not just close window). Test with: "Add a note called 'test' with content 'hello world'".

The Security Mistakes You Need to Avoid#

This is where most MCP tutorials stop. But shipping an insecure server is worse than shipping nothing. Here's what the research shows:

MCP Server Security Issues (2025 Research)

53% of MCP servers use insecure static secrets — hardcoded API keys that never expire. The State of MCP Security 2025 report found this is the #1 vulnerability.

43% have command injection vulnerabilities. When your tool takes user input and passes it to a shell command or database query, you need to sanitize.

Only 8.5% use OAuth properly. Most developers default to API keys because OAuth is harder. But for production servers handling user data, it's necessary.

What To Do#

For local-only servers (like our notes example): You're fine. Claude Desktop handles user identity. No network exposure.

For servers you'll publish:

  1. Never hardcode credentials. Use environment variables.
  2. Validate all inputs before using them in commands or queries.
  3. Implement OAuth if your server accesses external APIs on behalf of users.
# Bad: Command injection risk
@mcp.tool
def run_command(cmd: str) -> str:
    return os.popen(cmd).read()  # Never do this

# Good: Validate inputs, use safe APIs
@mcp.tool
def get_file(filename: str) -> str:
    if ".." in filename or filename.startswith("/"):
        return "Error: Invalid path"
    safe_path = Path("./data") / filename
    return safe_path.read_text() if safe_path.exists() else "Not found"

Production Patterns: Beyond the Tutorial#

When you're ready to deploy for real users, these patterns matter.

Persistent Storage#

Replace in-memory dict with SQLite:

import sqlite3
from pathlib import Path

DB = Path.home() / ".mcp-notes.db"

def get_db():
    conn = sqlite3.connect(DB)
    conn.execute("CREATE TABLE IF NOT EXISTS notes (title TEXT PRIMARY KEY, content TEXT)")
    return conn

Docker Deployment#

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY server.py .
USER 1000:1000
CMD ["python", "server.py"]

Key patterns from production MCP servers:

  • Minimal base images (alpine/distroless)
  • Non-root user
  • Health checks for orchestration
  • Secrets via environment variables, never in image

Kubernetes (For Scale)#

Real MCP deployments at scale use:

  • HPA for autoscaling on request volume
  • Readiness probes hitting /health endpoints
  • Secrets via cloud secret managers (AWS Secrets Manager, Azure Key Vault)
  • Workload identity instead of static credentials

The Azure AKS team published a reference architecture for MCP servers that's worth reading if you're going this route.

Common Errors and How to Fix Them#

After helping dozens of developers with their first MCP servers, these are the issues I see repeatedly:

ErrorCauseFix
"Tool not found"Typo in tool nameCheck exact spelling
Server hangsPrint to stdoutUse stderr for logging
"Connection refused"Server crashed on startupRun manually to see errors
Works in Inspector, fails in ClaudeConfig path issueUse absolute paths
Empty tool listDecorators not appliedCheck @mcp.tool syntax

The #1 mistake: Printing to stdout. Your server communicates with Claude via stdout. If you print("debug message"), you corrupt the JSON-RPC stream. Use print(..., file=sys.stderr) for debugging.

What to Build Next#

You've got the fundamentals. Here are ideas for servers that solve real problems:

High-value niches on MCPize:

  • Database connectors (PostgreSQL, MongoDB) — always in demand
  • Git/GitHub automation — developers love these
  • Calendar/task management — productivity gold
  • API wrappers for services without MCP support

What makes servers successful:

FactorWhy It Matters
Solves specific painGeneric servers lose to focused ones
Dead simple to configureComplex setup = abandoned installs
ReliableOne outage kills trust
Free tierDrives trials, converts to paid

The top-earning servers on MCPize make $500-2000/month. They're not complex — they just solve one problem exceptionally well.

Publish and Earn#

Ready to share your server?

pip install mcpize
mcpize init      # Creates manifest
mcpize dev       # Test locally
mcpize publish   # Ship it

You set the price. MCPize handles hosting, billing, auth. You keep 85% of revenue.

Learn How to Monetize

Frequently Asked Questions#

How long does it take to build a real MCP server?#

A simple server (like our notes example): 30 minutes. A production server with database, auth, and error handling: 2-4 hours. A full-featured server with multiple tools: 1-2 days.

Can I use async/await?#

Yes. Both FastMCP and the TypeScript SDK fully support async:

@mcp.tool
async def fetch_data(url: str) -> str:
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

My server works in Inspector but not in Claude?#

Check these in order:

  1. Config uses absolute path (not relative)
  2. Correct Python/Node in config (check which python)
  3. Claude Desktop fully restarted (not just window closed)
  4. View logs: tail -f ~/Library/Logs/Claude/mcp*.log

How much can I earn?#

Depends on the niche. Database connectors and productivity tools with clear value propositions perform best. Top servers earn $500-2000/month. The key is solving a specific problem well, not building a "does everything" tool.

Should I use Python or TypeScript?#

Python/FastMCP for rapid development and data-focused servers. TypeScript for production apps where type safety matters. Both are first-class citizens in the MCP ecosystem.

Summary#

In this MCP tutorial, you learned:

  • MCP's three primitives: tools (actions), resources (data), prompts (instructions)
  • Building servers in Python (FastMCP) and TypeScript (SDK)
  • Testing with MCP Inspector before Claude Desktop
  • Security patterns that 53% of developers get wrong
  • Production deployment with Docker and Kubernetes

The ecosystem is real. The protocol is stable. The opportunity is now.

Publish Your Server Explore MCP Servers

Questions? Join the MCPize Discord or explore more MCP guides.

Enjoyed this article?

Share it with your network

MCPize Team

MCPize Team

Core Team

The team behind MCPize - building the future of MCP server monetization.

Stay Updated

Get the latest MCP tutorials, product updates, and developer tips delivered to your inbox.

No spam, ever. Unsubscribe anytime.

Related Articles

Continue exploring similar topics

View all articles