Model Context Protocol (MCP) Complete Guide: Build, Connect, and Deploy Tool Servers (2026)
Master MCP from protocol fundamentals to production servers. Complete guide covering server architecture, tools, resources, prompts, transport layers, and client integration.
Model Context Protocol (MCP) Complete Guide: Build, Connect, and Deploy Tool Servers (2026)
Every AI agent needs tools, databases, APIs, file systems, browsers. Before MCP, each client (Claude Desktop, Cursor, custom agents) implemented its own integration layer. That meant duplicated work, inconsistent security models, and fragile one-off connectors. The Model Context Protocol (MCP), introduced by Anthropic and now adopted across the ecosystem, standardizes how AI applications discover and invoke external capabilities.
This guide covers MCP from protocol fundamentals through building production-grade servers in Python and TypeScript, connecting them to Claude Code, Cursor, and custom agent frameworks, and operating them securely at scale.
What You'll Learn
- MCP architecture: hosts, clients, servers, and the JSON-RPC protocol
- Three capability types: Tools, Resources, and Prompts
- Building MCP servers with the official Python and TypeScript SDKs
- Transport options: stdio, SSE, and streamable HTTP
- Security, auth, and production deployment patterns
What Is the Model Context Protocol?
MCP is an open protocol that defines how AI hosts (applications like Claude Desktop or Cursor) communicate with MCP servers: lightweight programs that expose tools, readable resources, and reusable prompt templates.
The relationship looks like this:
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β MCP Host ββββββΆβ MCP Client ββββββΆβ MCP Server β
β (Claude, β β (in-process β β (your code, β
β Cursor) β β connector) β β GitHub, DB)β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
Each host runs one MCP client per connected server. The client handles capability discovery, tool invocation, and resource reads over a standardized JSON-RPC 2.0 message format.
Why MCP Matters
- Write once, connect everywhere: one server works with Claude Desktop, Cursor, Claude Code, and custom agents
- Explicit capability contracts: tools declare JSON schemas; clients validate before invocation
- Separation of concerns: hosts handle UX and LLM routing; servers handle domain logic
- Composable ecosystem: community servers for GitHub, Postgres, Slack, filesystem, and more
Core Primitives: Tools, Resources, Prompts
Tools
Tools are functions the LLM can call. Each tool has a name, description, and input schema (JSON Schema):
# Conceptual tool definition via MCP SDK
@server.tool()
async def search_issues(query: str, repo: str) -> str:
"""Search GitHub issues in a repository."""
results = github_client.search_issues(q=f"{query} repo:{repo}")
return format_issues(results)
The LLM sees tool descriptions during capability negotiation and decides when to invoke them based on user intent.
Resources
Resources are readable data sources, files, database rows, API responses, identified by URIs:
file:///project/README.md
postgres://localhost/mydb/users/schema
github://repos/owner/repo/issues
Clients can list available resources and read their contents to inject into context without a full tool call round-trip.
Prompts
Prompts are reusable templates with arguments, exposed by servers for common workflows:
@server.prompt()
async def code_review(diff: str, language: str) -> str:
return f"Review this {language} code change:\n\n{diff}"
Hosts surface prompts in UI pickers, giving users one-click access to server-defined workflows.
Building an MCP Server in Python
Install the official SDK:
pip install mcp
Create a minimal stdio server:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import asyncio
server = Server("weather-server")
@server.list_tools()
async def list_tools():
return [
Tool(
name="get_forecast",
description="Get weather forecast for a city",
inputSchema={
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"days": {"type": "integer", "default": 3},
},
"required": ["city"],
},
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "get_forecast":
forecast = await fetch_weather(arguments["city"], arguments.get("days", 3))
return [TextContent(type="text", text=forecast)]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
TypeScript Server
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" });
server.tool(
"get_forecast",
{ city: z.string(), days: z.number().optional() },
async ({ city, days = 3 }) => {
const forecast = await fetchWeather(city, days);
return { content: [{ type: "text", text: forecast }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Transport Layers
stdio (Standard Input/Output)
The default for local integrations. The host spawns the server as a subprocess and communicates over stdin/stdout. Used by Claude Desktop, Cursor, and Claude Code.
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/path/to/weather_server.py"],
"env": { "API_KEY": "..." }
}
}
}
SSE (Server-Sent Events)
Remote servers over HTTP. The client connects to an SSE endpoint for server-to-client messages and POSTs client-to-server messages separately. Suitable for shared team servers.
Streamable HTTP
The newer transport replacing SSE in the MCP spec. Single HTTP endpoint handles bidirectional communication. Prefer this for new remote server deployments in 2026.
Connecting MCP Servers to Clients
Claude Desktop / Claude Code
Edit claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." }
}
}
}
Restart the client after config changes. Verify connection with /mcp in Claude Code.
Cursor
Configure MCP in Cursor Settings β MCP. Same JSON format as Claude Desktop. Cursor spawns servers on workspace open and exposes tools to the agent automatically.
Custom Agent Integration
Use the MCP client SDK to connect from LangGraph, CrewAI, or bespoke agents:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(
command="python",
args=["weather_server.py"],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
result = await session.call_tool("get_forecast", {"city": "London"})
Convert MCP tool schemas to LangChain or OpenAI function formats for your agent framework.
Security Best Practices
MCP servers run with the permissions of their host process. Treat every server as privileged code.
Principle of Least Privilege
A GitHub MCP server for issue triage needs issues:read and issues:write, not full repo admin. Scope tokens per server instance, not per user laptop.
Production Deployment Patterns
Sidecar Pattern
Run MCP servers as sidecar containers alongside your agent service in Kubernetes. The agent connects via stdio or local HTTP within the pod network.
Shared Remote Server
Deploy a centralized MCP gateway that authenticates users, routes to backend servers, and enforces policy. Useful when multiple teams share database or CRM access.
Serverless Considerations
Cold starts hurt stdio servers. For serverless, use streamable HTTP transport with warm pools or provisioned concurrency.
Error Handling and Observability
Return structured errors in tool responses rather than raising unhandled exceptions, the LLM can retry or explain failures to users:
@server.call_tool()
async def call_tool(name: str, arguments: dict):
try:
result = await execute(name, arguments)
return [TextContent(type="text", text=result)]
except PermissionError:
return [TextContent(type="text", text="Error: insufficient permissions for this operation.")]
except Exception as e:
logger.exception("Tool failure")
return [TextContent(type="text", text=f"Error: operation failed ({type(e).__name__})")]
Instrument servers with OpenTelemetry. Track tool call latency, error rates, and which tools the LLM invokes most frequently.
Testing MCP Servers
Use the MCP Inspector (npx @modelcontextprotocol/inspector) to interactively list tools, call them, and read resources during development.
Write integration tests that spawn the server subprocess and exercise the client SDK:
async def test_get_forecast():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("get_forecast", {"city": "Paris"})
assert "temperature" in result.content[0].text.lower()
Common Pitfalls
Overloaded tool descriptions. Vague descriptions cause wrong tool selection. Be explicit about when to use each tool and what it returns.
Huge tool responses. Return summaries or paginated results. Dumping 10,000 database rows into context wastes tokens and confuses the model.
Blocking I/O in async handlers. Use async HTTP clients and database drivers. Blocking calls stall the entire server.
Missing schema validation. Define strict JSON schemas with required fields and enum constraints.
Config drift across machines. Use dotfiles or team config repos to version MCP server definitions consistently.
Ecosystem and Community Servers
The MCP servers repository (modelcontextprotocol/servers) includes reference implementations for filesystem, memory, Git, Postgres, Puppeteer, and more. Treat these as starting points, fork and harden for production rather than connecting community servers directly to sensitive data.
When to Build vs. Buy
Build custom MCP servers when you have proprietary APIs, internal databases, or domain-specific workflows. Use community servers for standard integrations (GitHub, Google Drive) after security review.
MCP complements agent frameworks like LangGraph and CrewAI, it standardizes the tool layer so your orchestration code stays clean and your integrations stay portable.
Next Steps
Start with one read-only tool on a safe data source. Add write capabilities only after input validation and auth are solid. Connect to Claude Code or Cursor for interactive testing, then wire the same server into your production agent via the client SDK.