Build an MCP Server from Scratch: A Complete Developer Guide (2026)

If you want Claude, GPT, or Cursor to query your database, call APIs, or search local notes—but every model needs a different adapter—you are hitting the same N×M integration wall MCP was built to fix. This hands-on guide is for backend and AI developers with Python or TypeScript basics. By the end you will build, debug, and deploy a production-ready MCP Server with Tools, Resources, Prompts, HTTP transport, a knowledge-base project, and a five-step Mac cloud Runbook.

Developer workstation diagram showing an MCP Server connecting AI clients to external tools, databases, and file resources

Table of Contents

1. Introduction: Why AI Needs External Tools

LLMs are powerful text engines but blind to your live systems: training cutoffs block real-time data, and models cannot execute side effects without Tool Use. You want an assistant that reads your Markdown vault, runs SQL, or hits internal APIs—without rewriting integrations every time you switch from Claude to GPT or from Cursor to another IDE.

Three core pain points before MCP

  1. Fragmented tool wiring. OpenAI Function Calling, Claude Tool Use, and LangChain Tools each define schemas differently—N models × M tools = N×M adapters.
  2. No runtime discovery. REST endpoints sit in static docs; AI cannot call tools/list to learn what is available mid-session.
  3. Local Host dependency. Cursor and Claude Desktop spawn STDIO subprocesses on macOS; a Linux VPS or closed laptop breaks the chain.

Model Context Protocol (MCP) standardizes how Hosts discover and invoke Tools, read Resources, and load Prompt templates over JSON-RPC 2.0. This tutorial walks from zero to a deployable Server—not theory alone. For the protocol-level "why MCP is the HTTP of AI," see our MCP protocol deep dive.

2. What Is MCP? Protocol Before Code

Anthropic open-sourced MCP in November 2024. It sits between AI Clients (Claude Desktop, Cursor, custom apps) and the Server you build—exposing three capability types:

┌────────────────────┐ ┌─────────────────────┐ │ MCP Client │ ◄─────► │ MCP Server │ │ (Claude / Cursor) │ JSON │ (you build this) │ │ │ -RPC │ │ └────────────────────┘ └─────────────────────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ Tools Resources Prompts (actions) (read data) (templates)

Lifecycle: initialize handshake → capability negotiation → request/response → graceful shutdown. Transport options: STDIO (local subprocess) or HTTP + SSE / streamable HTTP (remote).

MCP vs Function Calling vs LangChain Tools

DimensionMCPOpenAI Function CallingLangChain Tools
StandardizationOpen protocolVendor-specificFramework-bound
TransportSTDIO / HTTPHTTP onlyHTTP only
Cross-modelYesNoPartial
Resources / PromptsNativeNot supportedNot supported
Runtime discoverytools/listStatic schemaStatic schema

3. Environment Setup

Choose your language

Python (recommended for this guide): official SDK mcp with FastMCP decorators. TypeScript: @modelcontextprotocol/sdk for Node-native teams.

# Python python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install "mcp[cli]" httpx pydantic # TypeScript (reference) npm init -y npm install @modelcontextprotocol/sdk

Project layout

my-mcp-server/ ├── server.py # entry point ├── tools/ │ ├── calculator.py │ ├── files.py │ ├── http_client.py │ ├── database.py │ └── datetime_tools.py ├── resources/ │ └── file_reader.py ├── prompts/ │ └── templates.py ├── tests/ │ └── test_tools.py ├── pyproject.toml └── README.md

Debug tooling

4. Hello World: Your First MCP Server

from mcp.server.fastmcp import FastMCP mcp = FastMCP("my-first-server") @mcp.tool() def say_hello(name: str) -> str: """Greet a person by name.""" return f"Hello, {name}! This is your first MCP tool." if __name__ == "__main__": mcp.run()

Run and inspect:

python server.py npx @modelcontextprotocol/inspector python server.py

Wire into Cursor (.cursor/mcp.json):

{ "mcpServers": { "my-first-server": { "command": "python", "args": ["/absolute/path/to/server.py"], "env": {} } } }

Restart Cursor, open MCP settings, and confirm say_hello appears in the tool list.

5. Tools: Five Practical Examples

Tools are actions. Function signatures plus docstrings become JSON Schema for the model. Use Pydantic models for complex inputs.

Tool 1 — Calculator

import ast import operator @mcp.tool() def calculate(expression: str) -> str: """Safely evaluate a math expression like '2 + 2 * 3'.""" ops = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, } def _eval(node): if isinstance(node, ast.Num): return node.n if isinstance(node, ast.BinOp): return ops[type(node.op)](_eval(node.left), _eval(node.right)) raise ValueError("Unsupported expression") return str(_eval(ast.parse(expression, mode="eval").body))

Tool 2 — File read/write

from pathlib import Path ALLOWED = Path("/data/notes").resolve() @mcp.tool() def read_file(path: str) -> str: """Read a text file under the allowed directory.""" target = (ALLOWED / path).resolve() if not str(target).startswith(str(ALLOWED)): raise PermissionError("Path outside allowed root") return target.read_text(encoding="utf-8") @mcp.tool() def write_file(path: str, content: str) -> str: """Write text to a file under the allowed directory.""" target = (ALLOWED / path).resolve() if not str(target).startswith(str(ALLOWED)): raise PermissionError("Path outside allowed root") target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content, encoding="utf-8") return f"Wrote {len(content)} bytes to {path}"

Tool 3 — HTTP request (async)

import httpx @mcp.tool() async def fetch_url(url: str, timeout: int = 10) -> str: """GET a URL and return response text (truncated to 8 KB).""" async with httpx.AsyncClient(timeout=timeout) as client: r = await client.get(url) r.raise_for_status() return r.text[:8192]

Tool 4 — Database query

import sqlite3 @mcp.tool() def query_sql(sql: str) -> list[dict]: """Run a read-only SELECT against the app SQLite database.""" if not sql.strip().upper().startswith("SELECT"): raise ValueError("Only SELECT queries allowed") conn = sqlite3.connect("app.db") conn.row_factory = sqlite3.Row rows = conn.execute(sql).fetchall() conn.close() return [dict(r) for r in rows]

Tool 5 — Time and timezone

from datetime import datetime from zoneinfo import ZoneInfo @mcp.tool() def now_in_timezone(tz: str = "UTC") -> str: """Return current time in an IANA timezone (e.g. America/New_York).""" return datetime.now(ZoneInfo(tz)).isoformat()

Best practices: return JSON-serializable types; use structured error strings instead of bare stack traces; set timeouts on network and DB calls; validate paths and SQL to prevent injection.

6. Resources: Dynamic Content for AI

Resources supply data; Tools perform actions. MCP addresses them by URI.

import json @mcp.resource("config://app-settings") def get_app_settings() -> str: """Return application configuration as JSON.""" return json.dumps({"version": "1.0.0", "env": "production"}) @mcp.resource("user://{user_id}/profile") def get_user_profile(user_id: str) -> str: """Return a user profile by ID.""" user = db.lookup(user_id) # your data layer return json.dumps(user)

Mime types: text/plain, application/json, or binary for PDFs/images. For a filesystem Server, expose directory listings and file contents as Resources so the model reads context without mutating state through Tools.

7. Prompts: Reusable Template Library

Prompts package multi-turn templates with parameters—useful for code review, incident triage, or interview prep.

from mcp.types import PromptMessage, TextContent @mcp.prompt() def code_review_prompt(language: str, code: str) -> list[PromptMessage]: """Structured code review template.""" return [ PromptMessage( role="user", content=TextContent( type="text", text=f"""Review this {language} code for: 1. Readability and structure 2. Bugs and security issues 3. Performance improvements ```{language} {code} ```""" ) ) ]

Hosts call prompts/list and prompts/get to inject consistent instructions—reducing prompt drift across sessions.

8. HTTP Transport: Remote MCP Servers

FeatureSTDIOHTTP + SSE / streamable HTTP
DeploymentLocal subprocessRemote server
LatencyVery lowNetwork-dependent
Multi-clientOne Host processMany clients
Best forCursor, Claude DesktopTeam SaaS, shared tools
from mcp.server.fastmcp import FastMCP mcp = FastMCP("remote-server", host="0.0.0.0", port=8000) # Add auth middleware in production: # - Bearer Token header validation # - API key per tenant # - CORS allowlist for web clients # - Rate limiting (e.g. 100 req/min) if __name__ == "__main__": mcp.run(transport="streamable-http")

Place Nginx or a cloud load balancer in front for TLS termination. Never expose an unauthenticated MCP HTTP endpoint to the public internet.

9. Debug and Test

MCP Inspector workflow

  1. Start: npx @modelcontextprotocol/inspector python server.py
  2. Call tools/list and verify schemas
  3. Invoke each Tool with edge-case inputs
  4. Inspect JSON-RPC request/response pairs in the UI

Automated test example

import pytest from mcp.client.session import ClientSession from mcp.client.stdio import StdioServerParameters, stdio_client @pytest.mark.asyncio async def test_calculator(): params = StdioServerParameters(command="python", args=["server.py"]) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool( "calculate", {"expression": "2 + 2"} ) assert "4" in result.content[0].text

Common errors

ErrorCauseFix
Tool missing in HostWrong path in mcp.jsonUse absolute paths; restart Host
JSON serialize failureNon-serializable returnReturn str or dict
Timeout disconnectSlow sync ToolMake async; add timeout
Permission deniedPath outside allowlistConfigure allowed roots

10. Production Deployment

Five-step production Runbook

Step 1 — Containerize

FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1 CMD ["python", "server.py"]

Step 2 — Transport and authentication

Enable streamable HTTP; require Bearer Token or API Key on every request; restrict CORS to known Host origins.

Step 3 — Test before ship

Run MCP Inspector plus pytest suite; record tools/call p95 latency baselines.

Step 4 — Observability

Structured JSON logs, Prometheus counters per tool name, Sentry for unhandled exceptions, and a /health endpoint for orchestrators.

Step 5 — Host on durable infrastructure

For STDIO workflows with Cursor, use launchd on a always-on Mac node with CPU and memory limits. For HTTP mode, deploy to Railway, Render, Cloud Run, or behind Nginx on a VPS—with TLS and auth enforced.

Declare MCP protocol version in initialize responses and version Tools carefully to avoid breaking existing Clients.

11. Project: Personal Knowledge Base MCP Server

Goal: Let Cursor answer "What did I write about MCP last week?" by searching local Markdown notes.

Stack

Core Tools

@mcp.tool() def index_notes(directory: str) -> str: """Scan Markdown files and rebuild the vector index.""" count = indexer.rebuild(directory) return f"Indexed {count} documents" @mcp.tool() def semantic_search(query: str, top_k: int = 5) -> list[dict]: """Search notes by meaning; return title, path, and snippet.""" return vector_store.search(query, k=top_k) @mcp.tool() def create_note(title: str, content: str) -> str: """Create a new Markdown note and index it.""" path = vault.create(title, content) indexer.add(path) return str(path)

Expose vault files as Resources (note://{slug}) for read-only preview. In Cursor, ask natural-language questions; the model calls semantic_search and cites snippets from your vault.

12. MCP Ecosystem Outlook

By mid-2026, MCP is supported across Claude Desktop, Cursor, VS Code Copilot extensions, OpenAI Responses API tooling, and a growing marketplace. Reference Servers worth studying:

Enterprise adoption is driving auth standards (OAuth 2.1, scoped API keys) and audited Server registries. Next steps: read the spec at modelcontextprotocol.io, publish your Server to GitHub, and combine MCP with Agent orchestration frameworks.

13. Hard Facts You Can Cite (2026-06-16)

14. Conclusion: From Tutorial to Production-Grade MCP

You now have the full path: protocol mental model, Python SDK setup, five Tools, Resources, Prompts, HTTP mode, Inspector testing, Docker packaging, and a knowledge-base reference project. Running this entirely on a local laptop works for demos—but lid-close sleep kills STDIO sessions, path and permission quirks multiply across teammates, and Linux VPS or Docker-only stacks cannot host native Cursor/Claude Desktop subprocesses or Apple toolchain sidecars without painful workarounds.

For stable, auditable, 7×24 MCP Servers that Cursor and Claude Desktop can reach reliably, deploy on a dedicated VPSMAC M4 Mac cloud node: bare-metal macOS, launchd KeepAlive, SSH in minutes, pin-able Python environments, and room to run HTTP gateways plus STDIO side-by-side. That is the production-grade 2026 path—more controllable than betting on a primary machine staying awake, and far simpler than forcing macOS Host workflows onto Linux VPS.