What Is an MCP Server and How Do You Build One from Scratch? A Detailed Guide
Learn MCP tools, resources, and prompts with runnable Python server code, stdio, Streamable HTTP, testing, OAuth, security, and production deployment.
· TankDev Engineering
MCP (Model Context Protocol) is an open client-server protocol that lets an AI application discover and invoke external tools, data resources, and reusable prompts through a standard interface. An MCP server is the program that publishes those capabilities as tools, resources, and prompts to MCP clients.
MCP is not a model and does not replace a database or business API. It is a contract layer between an AI host and operational systems: clients discover schemas, send validated arguments, receive structured results, and operate within an authorization boundary.
What is MCP?
The host is the IDE, assistant, or enterprise AI application. Its MCP client owns the protocol connection. The server publishes a bounded domain capability. The model does not receive a database password; it receives named, schema-defined operations.
| Primitive | Purpose | Example |
|---|---|---|
| Tool | Callable operation | Check or reserve stock |
| Resource | URI-addressable context | inventory://catalog/SKU-100 |
| Prompt | Reusable interaction template | Reservation review |
How MCP communication works
The client and server initialize and negotiate protocol versions and capabilities. Clients list tools, resources, or prompts before calling or reading them. Transport carries messages; authentication, tenant isolation, idempotency, and domain authorization remain application responsibilities.
Host -> MCP Client -> stdio or Streamable HTTP -> MCP Server
initialize -> capability negotiation -> list -> call/read/getDesign the contract before the server
- Choose one narrow domain.
- Separate reads from writes.
- Use precise names, descriptions, strict input schemas, and structured outputs.
- Require confirmation and idempotency for side effects.
- Allowlist tenants, records, fields, and downstream destinations.
- Set timeouts, limits, and maximum result size.
A hundred tools do not necessarily improve discovery. Large catalogs consume context and increase selection ambiguity. Split by domain and state when a tool must not be called.
Build an MCP server from scratch with Python
mkdir inventory-mcp && cd inventory-mcp
uv init
uv add mcp pydantic
uv add --dev pytest anyioThe following server exposes a read tool, a state-changing tool, a resource, and a prompt. Replace the in-memory dictionary with a tenant-aware application and transaction layer in production.
from __future__ import annotations
import json
from datetime import date
from typing import Annotated
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP(
"inventory",
instructions="Read inventory and create reservations only after user confirmation.",
)
STOCK = {"SKU-100": 12, "SKU-200": 0}
@mcp.tool()
def check_stock(
sku: Annotated[str, Field(pattern=r"^SKU-[0-9]{3}$")],
) -> dict:
"""Return current available units for one exact SKU."""
return {"sku": sku, "available": STOCK.get(sku, 0), "as_of": date.today().isoformat()}
@mcp.tool()
def reserve_stock(
sku: Annotated[str, Field(pattern=r"^SKU-[0-9]{3}$")],
quantity: Annotated[int, Field(ge=1, le=20)],
idempotency_key: Annotated[str, Field(min_length=16, max_length=128)],
) -> dict:
"""Reserve stock. This changes state and requires prior user confirmation."""
available = STOCK.get(sku, 0)
if quantity > available:
return {"ok": False, "code": "INSUFFICIENT_STOCK", "available": available}
STOCK[sku] = available - quantity
return {"ok": True, "sku": sku, "reserved": quantity, "idempotency_key": idempotency_key}
@mcp.resource("inventory://catalog/{sku}")
def catalog_item(sku: str) -> str:
"""Read-only catalog context addressable by URI."""
return json.dumps({"sku": sku, "available": STOCK.get(sku, 0)})
@mcp.prompt()
def reservation_review(sku: str, quantity: int) -> str:
"""Reusable prompt that asks the user to review a reservation."""
return f"Review reservation: SKU={sku}, quantity={quantity}. Do not execute without approval."
if __name__ == "__main__":
mcp.run() # stdio for a local host-launched server
Why the schema matters
Field bounds are executable contracts. Regex narrows identifiers, quantity limits constrain damage, and an idempotency key prevents retry duplication only when enforced by a tenant-scoped unique database constraint. A description asking for confirmation is not proof of confirmation; verify a host or authorization signal.
Run locally over stdio
The host launches the subprocess and carries protocol traffic over stdin/stdout. Reserve stdout for protocol bytes and send logs to stderr. Use absolute executable paths, a lockfile, and an environment allowlist rather than shell aliases or global Python state.
Deploy over Streamable HTTP
# server.py
if __name__ == "__main__":
mcp.run(transport="streamable-http")
# Start:
# uv run python server.py
# Endpoint:
# http://127.0.0.1:8000/mcp
# Client:
import asyncio
from mcp import Client
async def main():
async with Client("http://127.0.0.1:8000/mcp") as client:
print("protocol:", client.protocol_version)
print(await client.list_tools())
print(await client.call_tool("check_stock", {"sku": "SKU-100"}))
asyncio.run(main())
Production deployment adds TLS, trusted proxy handling, Host and Origin allowlists, body limits, lifecycle management, and OAuth. Browser CORS is not server-side Origin authorization. Do not expose private data from unauthenticated health routes.
Design tools, resources, prompts, and errors
Avoid generic run_sql, read_file, or call_url tools. Publish domain operations such as get_order and lookup_shipping_quote. Separate model-readable content from structured application output. Return stable error codes, retryability, and correlation IDs—not stack traces or SQL details.
| Risky surface | Risk | Narrow contract |
|---|---|---|
| run_query(sql) | Injection/data scope | get_order(order_id) |
| read_file(path) | Traversal/secrets | read_policy(policy_id) |
| call_api(url) | SSRF/credentials | shipping_quote(route_id) |
| quantity:int | Unbounded effect | 1 <= quantity <= 20 |
Test the server
import pytest
from mcp import Client
from server import mcp
@pytest.mark.anyio
async def test_check_stock_contract():
async with Client(mcp) as client:
tools = await client.list_tools()
assert {tool.name for tool in tools.tools} >= {"check_stock", "reserve_stock"}
result = await client.call_tool("check_stock", {"sku": "SKU-100"})
assert result.is_error is False
assert result.structured_content["sku"] == "SKU-100"
assert result.structured_content["available"] >= 0
Add subprocess tests for stdio discipline and HTTP tests for negotiation, authentication, Origin, timeouts, disconnects, and shutdown. Snapshot tools/list schemas to detect accidental breaking changes, but require semantic review before accepting a new snapshot.
MCP security
A model selecting a tool is not authorization. Derive subject, tenant, and scopes from verified credentials and enforce object-level access on every record. Never trust tenant_id supplied as a tool argument.
Treat resources, web pages, and tickets as untrusted data: embedded instructions cannot grant tool authority. Put high-impact tools behind policy and confirmation. Prevent SSRF with server-side destination allowlists, post-resolution IP checks, redirect limits, and audience-bound downstream credentials.
OAuth and remote authorization
A Streamable HTTP server is protected like an ordinary web service with bearer tokens and protected-resource metadata. Validate signature, issuer, audience, expiry, and scope. Keep user-delegated tokens distinct from service credentials. Stdio relies on the host’s process and environment boundary.
Production architecture
MCP transport -> authentication -> schema validation
-> authorization -> application service -> repository/API
-> structured result + auditGive each tool bounded timeout, concurrency, and circuit-breaker budgets. A slow browser tool must not exhaust fast read capacity. Retry only classified transient, idempotent operations; reconcile unknown side-effect outcomes before retry.
Observability and cost
- Protocol/server version and correlation ID.
- Queue, service, downstream, and total p50/p95/p99.
- Validation, denial, timeout, cancellation, and success.
- Payload/result size and truncation.
- Idempotency hit/conflict and unknown effects.
- Estimated context cost of tool schemas.
- Trace context without secrets or personal data.
Tool catalogs and large results consume model context even when the server does not generate tokens. Keep deterministic ordering, split domains, and return handles for large results.
Versioning and deployment checklist
Adding a required input or changing field meaning is a breaking change. Use optional additions, deprecation windows, schema-diff CI, consumer tests, and explicit new tool names or versions.
- Pinned SDK and image digest.
- Streamable HTTP behind TLS with Host/Origin policy.
- OAuth issuer, audience, expiry, and scope validation.
- Object authorization for every tool.
- Idempotency and confirmation for effects.
- Egress, filesystem, payload, time, and concurrency limits.
- Structured audit, contract tests, chaos tests, and rollback.
When not to use MCP
A fixed function call inside one application may not need MCP. Keep the existing domain API and add a narrow MCP adapter when standardized AI discovery is valuable. MCP does not replace authorization, queues, workflow engines, or business transactions.
Conclusion
An MCP server is a standardized capability boundary for AI applications. Production quality comes from small contracts, least privilege, structured output, idempotent effects, secure transport, testing, and observability—not from publishing the largest possible tool catalog.
Continue with multi-agent failure isolation, event-driven LLM workers, and AI automation reliability.
MCP frequently asked questions
01What is MCP?
An open client-server protocol for discovering and invoking AI tools, resources, and prompts through standard contracts.
02What is an MCP server?
A program that exposes tools, resources, and prompts over stdio or Streamable HTTP.
03How does MCP differ from REST?
REST is a general HTTP API style; MCP adds AI-oriented capability discovery and primitive schemas, often as an adapter over domain APIs.
04Which language can I use?
The protocol is language-neutral; Python and TypeScript have widely used official SDKs.
05Stdio or Streamable HTTP?
Use stdio for local host-launched processes and Streamable HTTP for deployed multi-client services.
06Should new servers use SSE?
No. Legacy SSE remains for older clients; new deployments should use Streamable HTTP.
07Tool or resource?
A tool performs an operation; a resource exposes URI-addressable context.
08Is MCP secure by default?
No. Authentication, authorization, confirmation, idempotency, egress policy, and audit must be designed.
09Is a tool call user approval?
No. Model selection is neither authorization nor verified confirmation.
10How do I test an MCP server?
Use in-memory contract tests, stdio subprocess tests, HTTP auth/transport tests, and downstream chaos tests.
11Can MCP servers scale?
Yes. Streamable HTTP services can scale behind load balancers when state and downstream limits are designed explicitly.
12How many tools should a server expose?
No universal number; prefer a narrow domain and remove redundant or unused tools.