If you've been building AI agents for more than a few months, you've felt the integration problem. Your agent needs to read files. Call an API. Query a database. Search internal documentation. And each one of those connections is a custom integration — written once, maintained forever, coupled to your specific agent framework.
Model Context Protocol (MCP) is the standardization layer that this ecosystem needed. It defines a universal interface between AI models and the external tools and data sources they interact with. Not a library, not a framework — a protocol, like HTTP. Build an MCP server once, and any MCP-compatible client can use it.
This is the practical guide. How MCP actually works, what you need to understand as a developer, and a real MCP server implementation you can adapt.
What MCP Is (and What It Isn't)
MCP is a client-server protocol. The AI model or agent framework is the client. The tool or data source is the server. The protocol defines how they communicate: how the client discovers what the server can do, how it requests operations, and how the server returns results.
What MCP is:
- An open protocol specification (not proprietary)
- A JSON-RPC-based communication standard
- A way to expose tools, resources, and prompts to any compatible AI client
- A separation of concerns: your AI logic and your data access logic no longer need to know about each other's internals
What MCP is not:
- A specific library (though SDKs exist for Python, TypeScript, etc.)
- An agent framework
- A replacement for your vector database or any specific technology
- Magic — it doesn't make integration easier, it makes integration standardized
The analogy that clarifies: REST APIs didn't make it easier to build backends. They made it possible for different clients to talk to those backends without custom protocols for each combination. MCP does the same for AI tool access.
The Three Things an MCP Server Can Expose
MCP servers expose three types of capabilities:
Tools — functions the AI can call. A tool has a name, a description (used by the AI to decide when to call it), and input parameters. When the AI calls a tool, the server executes it and returns the result.
{
"name": "query_database",
"description": "Run a SQL query against the product database. Use this to answer questions about products, inventory, or orders.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Valid SQL SELECT query"
}
},
"required": ["query"]
}
}
Resources — data the AI can read. Resources are like files or documents — the AI can request them, but it doesn't actively call them like a function. Examples: a configuration file, a document from your knowledge base, the current state of some data.
Prompts — reusable prompt templates the server exposes. Less commonly used, but allows servers to define task-specific prompt templates that clients can request.
In practice, 80% of what you'll build with MCP is tools. Resources matter for data access patterns; prompts are niche.
How the Protocol Works
MCP uses JSON-RPC 2.0 over either stdio (for local tools, launched as a subprocess) or SSE/HTTP (for remote servers). The lifecycle:
- Initialization — client connects, server returns its capabilities (list of tools, resources, prompts it supports)
- Discovery — client calls
tools/list,resources/listto enumerate what's available - Invocation — client calls
tools/callwith the tool name and arguments - Result — server executes and returns structured results
The transport detail matters for how you deploy:
- stdio transport — your MCP server is a local subprocess. The client spawns it, communicates over stdin/stdout. Simple, no network required, best for development tools (IDE integrations, local file access, shell commands)
- SSE/HTTP transport — your MCP server is a web server. The client makes HTTP requests. Required for remote tools, multi-user scenarios, or tools that need to stay alive between calls
Building Your First MCP Server
Python implementation using the mcp SDK. This example builds a database query server — a realistic tool that agents need constantly:
# pip install mcp sqlalchemy
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
from sqlalchemy import create_engine, text
from typing import Any
# Initialize server
app = Server("database-server")
# Database setup
engine = create_engine("postgresql://user:pass@localhost/mydb")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="query_database",
description=(
"Execute a read-only SQL query against the product database. "
"Use this to look up products, check inventory, or analyze orders. "
"Only SELECT queries are allowed."
),
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL SELECT query to execute"
},
"limit": {
"type": "integer",
"description": "Maximum rows to return (default: 50, max: 200)",
"default": 50
}
},
"required": ["query"]
}
),
types.Tool(
name="list_tables",
description="List all available tables in the database with their column names.",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
if name == "query_database":
query = arguments["query"]
limit = min(arguments.get("limit", 50), 200)
# Security: only allow SELECT
if not query.strip().upper().startswith("SELECT"):
return [types.TextContent(
type="text",
text="Error: Only SELECT queries are permitted."
)]
try:
with engine.connect() as conn:
result = conn.execute(
text(f"{query} LIMIT :limit"),
{"limit": limit}
)
rows = result.fetchall()
columns = result.keys()
# Format as readable table
if not rows:
return [types.TextContent(type="text", text="Query returned no results.")]
output = " | ".join(columns) + "\n"
output += "-" * len(output) + "\n"
for row in rows:
output += " | ".join(str(v) for v in row) + "\n"
return [types.TextContent(type="text", text=output)]
except Exception as e:
return [types.TextContent(type="text", text=f"Query error: {str(e)}")]
elif name == "list_tables":
try:
with engine.connect() as conn:
result = conn.execute(text(
"SELECT table_name, column_name, data_type "
"FROM information_schema.columns "
"WHERE table_schema = 'public' "
"ORDER BY table_name, ordinal_position"
))
rows = result.fetchall()
tables: dict[str, list[str]] = {}
for table, column, dtype in rows:
if table not in tables:
tables[table] = []
tables[table].append(f"{column} ({dtype})")
output = ""
for table, columns in tables.items():
output += f"\n{table}:\n"
for col in columns:
output += f" - {col}\n"
return [types.TextContent(type="text", text=output)]
except Exception as e:
return [types.TextContent(type="text", text=f"Error: {str(e)}")]
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
Run this: python server.py. It waits on stdin for MCP protocol messages.
The Tool Description Problem
The single biggest factor in whether your MCP server works well is the tool description. Not the implementation — the string of text that tells the AI what the tool does and when to use it.
The AI decides whether to call your tool based entirely on that description. Bad description: tool gets called at the wrong time, or never. Good description: tool gets called exactly when it should.
Bad description:
"Queries the database"
Good description:
"Execute a read-only SQL query against the product database.
Use this to look up products by name or SKU, check current
inventory levels, retrieve order history, or analyze sales data.
Only SELECT queries are allowed. Call list_tables first if you
don't know the schema."
The good version tells the AI: when to use it, what kind of questions it can answer, and constraints. The Call list_tables first if you don't know the schema instruction is the most important part — it teaches the AI the correct usage pattern.
Authentication and Security Patterns
MCP servers run with the permissions of the process that executes them. This is both flexible and dangerous.
For local stdio servers: They run with your user's permissions. A file-reading MCP server can read anything your user can read. This is fine for personal tools, dangerous for shared infrastructure.
For HTTP/SSE servers: Implement authentication at the transport layer. The MCP spec supports auth headers; use them. Options:
- API key in HTTP headers (simplest, good for internal tools)
- OAuth 2.0 for user-delegated access (complex, correct for consumer products)
- mTLS for service-to-service (highest security, infrastructure complexity)
For the database server above: create a read-only database user specifically for the MCP server. Never give an MCP server credentials that can write unless it needs to.
Input validation matters. The AI will pass arguments that match the schema, but it can construct adversarial inputs (intentionally or through prompt injection attacks). Validate all inputs before execution. The SQL example above only allows SELECT — that's not a feature, it's a security requirement.
Real MCP Servers Worth Knowing
The MCP ecosystem has grown rapidly. Useful servers to know about:
- filesystem — read/write files in a specified directory. The standard starting point for any local tool use.
- git — read commits, diffs, branches. Useful for code review agents.
- fetch — fetch URLs and return cleaned content. Web access for agents.
- sqlite / postgres — database access with configurable permissions.
- github — read/write issues, PRs, repositories. More capable than fetch for GitHub-specific use cases.
All of these are open source, available on GitHub, and MCP-compatible. You can use them directly or study them to understand implementation patterns.
Where MCP Fits in Your Architecture
MCP doesn't replace your agent framework. It extends it. The typical architecture:
User request
↓
Agent (LangGraph / your framework)
↓
MCP Client (built into your framework or via SDK)
↓ ↓ ↓
MCP Server A MCP Server B MCP Server C
(database) (file system) (web search)
Your agent logic decides when to call tools. MCP defines how to call them. The separation means you can swap out individual MCP servers without touching your agent logic, and swap out your agent framework without rewriting your tool implementations.
This is the architectural promise of MCP — and in 2026, it's starting to deliver. More agent frameworks are adding native MCP support. More tools are shipping MCP servers. The ecosystem is approaching the critical mass where "does it have an MCP server?" is a reasonable question to ask about any data source or API.
Phase 7 of MindloomHQ's Agentic AI course covers MCP in depth — building MCP servers, connecting them to LangGraph agents, and the production patterns for tool architecture in real systems. Explore the curriculum →