Introduction
The Model Context Protocol (MCP) has become the universal standard for connecting AI assistants to external tools and data sources. As of May 2026, the TypeScript SDK has crossed 150 million downloads, over 13,000 MCP servers exist on GitHub, 28% of Fortune 500 companies have implemented MCP servers in their AI stacks, and Gartner predicts 75% of API gateway vendors will include MCP support by end of 2026. In December 2025, Anthropic donated MCP to the Linux Foundation’s Agentic AI Foundation, cementing its vendor-neutral governance.
Build an MCP server once, and every MCP-compatible client — Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, Zed, ChatGPT — can use it immediately. The protocol solves the N×M problem: without MCP, connecting N AI models to M tools requires N×M custom integrations. With MCP, the math reduces to N+M: build one server per tool, and every client can use every server.
This guide teaches you how to build custom MCP servers from scratch, covering both TypeScript and Python, both transport options, and production security. For a conceptual overview of MCP, see the MCP Complete Guide.
Understanding MCP Architecture
Protocol Overview
MCP uses a client-server architecture with three distinct roles:
| Role | Examples | Responsibility |
|---|---|---|
| Host | Claude Desktop, Cursor, VS Code | Initiates connections, owns model and UI |
| Client | One per connected server, inside the host | JSON-RPC messaging, capability negotiation |
| Server | Your process | Exposes tools, resources, prompts over a transport |
Connection Lifecycle
Host → Client → Initialize (version + capabilities)
← Negotiate (supported features)
→ Initialized notification
←→ JSON-RPC message exchange
→ Shutdown (either side)
The protocol is stateful — once a session is established, both sides maintain context. All communication uses JSON-RPC 2.0 with well-defined jsonrpc, method, params, and result/error fields.
Six Capabilities
MCP servers can expose three capabilities, and clients can optionally offer three back to servers:
| Capability | Controlled By | Purpose |
|---|---|---|
| Tools | Model (with user approval) | Functions the AI can call — query DB, send email, call API |
| Resources | Host (fetched as context) | URI-addressed read-only data — files, docs, DB rows |
| Prompts | User (slash commands) | Reusable templates — /summarize, /review-pr |
| Sampling | Server → Host model | Server asks the host’s LLM to complete text mid-call |
| Roots | Host → Server | Host declares which URIs the server may operate on |
| Elicitation | Server → User | Server asks for additional user input mid-call |
A practical heuristic: if the model needs to do something, make it a tool. If the model needs to read something, make it a resource. If the user wants a named shortcut, make it a prompt.
Transport Options
| Transport | Use When | Notes |
|---|---|---|
| stdio | Local servers, IDE integrations | Server runs as subprocess, JSON-RPC over stdin/stdout |
| Streamable HTTP | Remote servers, multi-user, production | Single HTTP endpoint, POST for requests + optional SSE |
Important: The HTTP+SSE transport from the original 2024-11-05 spec was deprecated in March 2025. Do not build new servers on it. Use Streamable HTTP for remote deployments.
Rule of thumb: start with stdio — zero network config, and the MCP Inspector debugs it natively. Ship Streamable HTTP only when you need remote access, SSO, or multi-user deployment.
Setting Up Your Environment
TypeScript Setup
# Create and initialize project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
# Install MCP SDK and Zod for input validation
npm install @modelcontextprotocol/sdk zod
# TypeScript tooling
npm install -D typescript tsx @types/node
npx tsc --init
Python Setup
# Create project with uv (recommended) or pip
uv add "mcp[cli]"
# or: pip install "mcp[cli]"
# For the FastMCP high-level API (recommended for new projects)
uv add fastmcp
# or: pip install fastmcp
Project Structure
mcp-server/
├── src/
│ ├── index.ts # TypeScript entry
│ └── server.py # Python entry (pick one language)
├── package.json # TypeScript deps
├── pyproject.toml # Python deps
└── README.md
Building Your First MCP Server
Every MCP server follows the same pattern: instantiate a server object, register capabilities (tools, resources, prompts), connect a transport, and handle requests.
TypeScript with McpServer (High-Level API)
The McpServer class handles all protocol details automatically. Zod schemas provide input validation:
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: "weather-server",
version: "1.0.0",
});
// Register a tool with Zod input schema
server.tool(
"get_forecast",
"Get weather forecast for a city",
{
city: z.string().describe("City name"),
days: z.number().optional().default(3).describe("Number of days"),
},
async ({ city, days }) => {
const response = await fetch(
`https://api.weather.gov/points/${encodeURIComponent(city)}`
);
const data = await response.json();
return {
content: [{ type: "text", text: `Forecast for ${city}: ${data.properties.periods[0].detailedForecast}` }],
};
}
);
// Register a resource (URI-addressed data)
server.resource(
"server-config",
"config://server",
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({ version: "1.0", env: "production" }),
}],
})
);
// Connect stdio transport and start
const transport = new StdioServerTransport();
await server.connect(transport);
Run with: npx tsx src/index.ts
Critical: Never write to stdout outside the SDK. console.log corrupts the JSON-RPC stream. Use console.error for logging — clients forward stderr to their debug logs.
Python with FastMCP
FastMCP uses Python decorators and type hints to auto-generate JSON Schema from function signatures:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
"""Get weather forecast for a city."""
import httpx
response = httpx.get(f"https://api.weather.gov/points/{city}")
data = response.json()
return f"Forecast for {city}: {data['properties']['periods'][0]['detailedForecast']}"
@mcp.resource("config://server")
def get_config() -> str:
"""Server configuration."""
return '{"version": "1.0", "env": "production"}'
if __name__ == "__main__":
mcp.run(transport="stdio")
Run with: uv run src/server.py or python src/server.py
FastMCP handles serialization, error formatting, and transport setup automatically. The @mcp.tool() decorator reads the function name, docstring, and type hints to produce the tool definition. For rapid prototyping, this is the fastest path to a working server.
Low-Level API (TypeScript)
If you need fine-grained control over protocol handling, use the lower-level Server class:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "weather-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: "get_forecast",
description: "Get weather forecast for a city",
inputSchema: {
type: "object",
properties: {
city: { type: "string", description: "City name" },
},
required: ["city"],
},
}],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Handle tool execution
return { content: [{ type: "text", text: `Result for ${name}` }] };
});
const transport = new StdioServerTransport();
await server.connect(transport);
Streamable HTTP Transport (Remote Servers)
For remote or multi-tenant deployments, use Streamable HTTP. A single endpoint handles all requests, with optional SSE streaming for server-initiated messages.
TypeScript with Express
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { randomUUID } from "node:crypto";
import { z } from "zod";
const app = express();
app.use(express.json());
const server = new McpServer({ name: "remote-server", version: "1.0.0" });
server.tool("get_forecast", "Weather forecast",
{ city: z.string() },
async ({ city }) => ({ content: [{ type: "text", text: `Weather in ${city}` }] })
);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
allowedHosts: ["localhost:3000", "mcp.example.com"],
allowedOrigins: ["https://claude.ai", "https://cursor.sh"],
enableDnsRebindingProtection: true,
});
await server.connect(transport);
app.all("/mcp", (req, res) => transport.handleRequest(req, res, req.body));
app.listen(3000, "127.0.0.1");
Three non-negotiable defaults: validate the Origin header on every request, bind to 127.0.0.1 for local servers, and authenticate remote servers (OAuth 2.1 with PKCE via the SDK’s mcpAuthRouter, or a signed JWT for internal use).
Python FastMCP with Streamable HTTP
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("remote-server")
@mcp.tool()
def get_forecast(city: str) -> str:
return f"Weather in {city}"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
For serverless-friendly deployments, pass stateless_http=True and json_response=True.
Connecting to MCP Clients
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"weather-server": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"]
}
}
}
For Python servers deployed via pip:
{
"mcpServers": {
"weather-server": {
"command": "uvx",
"args": ["weather-server@latest"]
}
}
}
Cursor
Create .cursor/mcp.json in your project:
{
"mcpServers": {
"weather-server": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"]
}
}
}
VS Code
Create .vscode/mcp.json in your project:
{
"servers": {
"weather-server": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/dist/index.js"]
}
}
}
Claude Code (CLI)
claude mcp add weather-server -- node /absolute/path/to/dist/index.js
# For remote servers
claude mcp add weather-server --transport http --url https://mcp.example.com/mcp
Always use absolute paths for stdio servers — clients spawn subprocesses from their own working directory. Quit and relaunch the client after editing config.
Security Best Practices
Security is the hardest part of MCP server development. Researchers disclosed 30+ MCP-related CVEs in the first four months of 2026, including a CVSS 9.6 command-injection RCE in mcp-remote (downloaded ~500K times) and a zero-interaction prompt injection in Windsurf (CVE-2026-30615).
Threat Categories and Defenses
Tool Poisoning — A malicious server embeds hidden instructions in a tool’s description or parameter names. The model reads those as user intent. Defense: only install servers from trusted sources, treat the annotations field as untrusted per spec.
Command Injection — Interpolating client-supplied strings into shell commands or SQL. The mcp-remote RCE happened exactly this way. Defense: parameterize every shell and DB call, validate with Zod or Pydantic before dispatch, prefer execFile over shell invocations.
DNS Rebinding — An attacker’s page tricks a browser into hitting http://localhost:3000/mcp. Defense: the Origin and Host allowlist shown in the Streamable HTTP example plus localhost-only binding.
Over-Scoped Filesystem Access — The server runs as your user. Defense: accept an explicit roots allowlist; never read .env*, ~/.ssh/**, ~/.aws/** without a whitelist.
Confused-Deputy / Cross-Server Exfiltration — A poisoned server instructs the model to read data from another server and exfiltrate it. Defense: fine-grained tool approvals on the host side and least-privilege scopes per server.
OWASP Guidelines (February 2026)
| Practice | Implementation |
|---|---|
| Input validation | Validate all tool inputs against strict Zod/Pydantic schemas |
| Least privilege | Scoped API keys, read-only DB connections, restricted filesystem paths |
| User consent | Every tool invocation requires explicit user approval |
| Rate limiting | Cap tool calls per session to prevent runaway AI loops |
| Transport security | TLS 1.3 for remote servers; sandboxed subprocess for stdio |
| Audit logging | Log every invocation with timestamps, params, and results |
OAuth 2.1 (Spec 2025-06-18)
The June 2025 spec changed how authentication works. MCP servers are now OAuth Resource Servers, not Authorization Servers. They consume access tokens issued by your existing identity provider (Auth0, Okta, Keycloak) rather than issuing their own tokens. This aligns MCP with established enterprise auth patterns.
Testing with MCP Inspector
MCP Inspector is the official debugger — a React UI plus a proxy process that speaks any transport.
# Inspect a TypeScript server in development
npx @modelcontextprotocol/inspector node dist/index.js
# Inspect a Python server
npx @modelcontextprotocol/inspector uv run server.py
# Inspect a published npm server
npx @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem ~/Documents
# Inspect a running Streamable HTTP server
npx @modelcontextprotocol/inspector --transport http --url http://localhost:3000/mcp
The UI opens at http://localhost:6274 with tabs for Tools, Resources, Prompts, and a Notifications pane showing all JSON-RPC traffic — the fastest way to verify schemas and catch capability-negotiation errors.
Best Practices
Tool Design
- Scope: One MCP server per coherent product area — a
jira-server,postgres-server,stripe-server— not a “platform-server” with 80 tools. - Count limit: Agent quality degrades sharply past 30–40 tools per context (Anthropic internal evaluations). Keep each server focused.
- Names: Use verbs for tool names (
get_forecast,create_ticket), keep descriptions under 1K tokens, prefer one multi-arg tool over many near-duplicates. - Errors: Always return structured responses with
{ content: [...], isError: true }on failure.
Development Workflow
- Start with stdio — zero network config, Inspector works natively
- Use the high-level API —
McpServer(TypeScript) orFastMCP(Python) for all new projects - Never write to stdout — corrupts JSON-RPC for stdio transport
- Validate inputs — Zod for TypeScript, Pydantic type hints for Python
- Ship Streamable HTTP only when needed — remote access, SSO, multi-user
Common Pitfalls
- Building a 50-tool monolith (split into focused servers)
- Using HTTP+SSE transport (deprecated; use Streamable HTTP)
- Writing to stdout (
console.loginstead ofconsole.error) - Relative paths in client configs (must be absolute)
- Not restarting the client after config changes (close the window and relaunch)
Real-World MCP Use Cases
| Use Case | MCP Server | Value |
|---|---|---|
| Database Assistant | Read-only SQL query tool + schema resource | Query production data in natural language |
| CI/CD Integration | GitHub Actions wrapper tools | Trigger builds, check status from IDE |
| Documentation Search | Internal docs as resources | AI answers from your actual docs |
| Customer Support | CRM + ticketing system connectors | AI-powered context from Salesforce, Zendesk |
| Infrastructure Ops | AWS/GCP API wrappers | Manage cloud resources through natural language |
| Code Review | Linting + testing + static analysis tools | Automated code reviews with real tool output |
The common thread: MCP servers turn existing APIs into AI-accessible capabilities without modifying the underlying systems. Your database does not need to know about MCP. The server sits in between and translates.
Spec Timeline and Ecosystem
| Date | Milestone |
|---|---|
| Nov 2024 | Anthropic releases MCP as open standard |
| Mar 2025 | Streamable HTTP replaces SSE transport |
| Jun 2025 | OAuth 2.1, elicitation, structured output |
| Nov 2025 | Latest stable spec (2025-11-25) with refinements |
| Dec 2025 | Donated to Linux Foundation’s Agentic AI Foundation |
| Q1 2026 | TypeScript SDK v2 stable expected |
The Linux Foundation governance is the most consequential 2026 development for MCP. Vendor-neutral governance converts a de facto standard into an actual one — no single company controls the protocol’s evolution.
Conclusion
MCP has become the USB-C for AI — a universal connector that replaces the mess of custom integrations. With 150M+ SDK downloads, 13K+ servers, support from every major AI platform, and neutral governance under the Linux Foundation, it is the standard for connecting AI to tools.
Build focused servers (one per product area, under 30 tools), start with stdio, use the high-level API, validate every input, and ship Streamable HTTP only when you need remote access. Following these patterns will produce servers that work immediately in Claude, Cursor, VS Code, and every other MCP-compatible client.
Resources
- Official Build-a-Server Tutorial (Anthropic) — Multi-language walkthroughs
- MCP Specification (2025-06-18) — Authoritative protocol spec
- TypeScript SDK — Production SDK, 150M+ downloads
- Python SDK — FastMCP decorators and Streamable HTTP
- MCP Inspector — Official debugger
- OWASP MCP Security Guide — Production security best practices
- MCP Servers Registry — Official server list
Comments