What is MCP (Model Context Protocol) and Why Should AI Engineers Learn It Early?
Problem
I spent months learning different AI frameworks. LangChain, CrewAI, AutoGen, Semantic Kernel. Each one had its own way of connecting to tools:
from langchain.tools import Tool
tool = Tool( name="search", func=search_function, description="Search the web")from crewai_tools import tool
@tool("Search Tool")def search_function(query: str) -> str: return search_resultsfrom autogen import register_function
register_function( search_function, caller=agent, executor=user_proxy, name="search")Three frameworks, three different tool definitions. When I switched from LangChain to CrewAI, I rewrote all my tools. When I wanted to try Claude’s agent features, I had to rewrite again.
Then I discovered MCP on a Reddit thread:
“MCP is worth learning early, it’s becoming the standard way agents connect to external tools” - Deep_Ad1959
Environment
- Python 3.11+
- Claude Desktop or Claude API
- MCP SDK (mcp Python package)
What is MCP?
MCP (Model Context Protocol) is an open standard developed by Anthropic that provides a unified way for AI models to connect with external tools, data sources, and systems.
Think of it like USB for AI tools:
┌─────────────────────────────────────────────────────────┐│ AI Application ││ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││ │ Claude │ │ GPT │ │ Gemini │ ││ └────┬────┘ └────┬────┘ └────┬────┘ ││ │ │ │ ││ └────────────┼────────────┘ ││ │ ││ ▼ ││ ┌─────────┐ ││ │ MCP │ ← One standard protocol ││ │ Protocol│ ││ └────┬────┘ ││ │ │└───────────────────┼─────────────────────────────────────┘ │ ┌───────────┼───────────┬───────────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │PostgreSQL│ │ File │ │ GitHub │ │ Web │ │ Server │ │ Server │ │ Server │ │ Search │ └─────────┘ └─────────┘ └─────────┘ └─────────┘Before MCP, each AI platform had its own tool integration. With MCP, you write one server, and any MCP-compatible client can use it.
The Three Core Primitives
MCP provides three primitives for connecting AI to external systems:
1. Resources (Read Data)
Resources let AI read data from your systems:
from mcp.server import Serverfrom mcp.server.stdio import stdio_server
app = Server("my-resource-server")
@app.list_resources()async def list_resources(): return [ Resource( uri="file:///logs/app.log", name="Application Logs", mimeType="text/plain" ), Resource( uri="database:///users", name="User Database", mimeType="application/json" ) ]
@app.read_resource()async def read_resource(uri: str): if uri == "file:///logs/app.log": with open("/var/log/app.log") as f: return f.read() # Handle other URIs...Resources are read-only. The AI can query your database, read files, fetch API data - but cannot modify anything.
2. Tools (Execute Actions)
Tools let AI perform actions:
from mcp.server import Serverfrom mcp.types import Tool, TextContent
app = Server("my-tool-server")
@app.list_tools()async def list_tools(): return [ Tool( name="create_user", description="Create a new user account", inputSchema={ "type": "object", "properties": { "email": {"type": "string"}, "name": {"type": "string"} }, "required": ["email", "name"] } ), Tool( name="send_notification", description="Send a notification to a user", inputSchema={ "type": "object", "properties": { "user_id": {"type": "string"}, "message": {"type": "string"} }, "required": ["user_id", "message"] } ) ]
@app.call_tool()async def call_tool(name: str, arguments: dict): if name == "create_user": user = await create_user_in_db( email=arguments["email"], name=arguments["name"] ) return [TextContent( type="text", text=f"Created user {user.id}" )] # Handle other tools...Tools can modify data, call APIs, send emails - anything you allow.
3. Prompts (Reusable Templates)
Prompts are pre-defined templates:
from mcp.server import Serverfrom mcp.types import Prompt
app = Server("my-prompt-server")
@app.list_prompts()async def list_prompts(): return [ Prompt( name="analyze_logs", description="Analyze application logs for errors", arguments=[ {"name": "service", "required": True} ] ), Prompt( name="code_review", description="Review code for best practices", arguments=[ {"name": "file_path", "required": True} ] ) ]
@app.get_prompt()async def get_prompt(name: str, arguments: dict): if name == "analyze_logs": logs = await fetch_logs(arguments["service"]) return f"""Analyze these logs for errors and anomalies:
{logs}
Provide:1. Summary of issues found2. Root cause analysis3. Recommended fixes"""Prompts help standardize common tasks across your team.
How I Connected MCP to Claude
I built a simple MCP server and connected it to Claude Desktop:
{ "mcpServers": { "my-tools": { "command": "python", "args": ["/path/to/my_mcp_server.py"], "env": { "DATABASE_URL": "postgresql://localhost/mydb" } } }}The config goes in:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
When I restart Claude Desktop, my tools appear automatically:
[2026-03-13 10:15:00] MCP Server "my-tools" started[2026-03-13 10:15:00] Available tools:[2026-03-13 10:15:00] - create_user[2026-03-13 10:15:00] - send_notification[2026-03-13 10:15:00] Available resources:[2026-03-13 10:15:00] - file:///logs/app.log[2026-03-13 10:15:00] - database:///usersNow I can ask Claude:
User: Create a new user account for [email protected] named "John Doe"
Claude: I'll use the create_user tool to set up the account.[Uses create_user tool]Created user usr_12345. The account is ready.Why MCP Matters
I used to think tool integration was a solved problem. Then I hit three walls:
1. Framework Churn
Every few months, a new “best” framework appeared:
2023: "Use LangChain"2024: "Use CrewAI"2024: "Use AutoGen"2025: "Use LangGraph"Each framework had its own tool system. I rewrote integrations three times.
With MCP, I write once:
# One MCP server works with:# - Claude Desktop# - Claude API# - Any future MCP client# - Multiple frameworks simultaneously2. Vendor Lock-in
My tools only worked with one AI provider:
# LangChain tools only worked with LangChain agents# OpenAI functions only worked with OpenAI models# Claude tools only worked with Claude
# Result: Switching providers = rewriting everythingMCP separates tools from AI providers. Write one server, use with any MCP-compatible AI.
3. Integration Complexity
Each integration had different patterns:
LangChain: Tool class with func + descriptionCrewAI: @tool decorator with different signaturesAutoGen: register_function with executor patternI spent more time on tool plumbing than on my actual application.
The Real Benefit: Transferable Skills
A Reddit user baconboy-957 said: “Use Claude code to understand Python, JavaScript, and APIs in general. Learn MCPs and how they work.”
This is the key insight. When I learned MCP:
Before MCP:- Spent 2 weeks learning LangChain tool system- Spent 1 week learning CrewAI tool system- Spent 1 week learning OpenAI function calling
After MCP:- Spent 3 days learning MCP- Applied to all frameworks instantlyThe MCP skills transfer because it’s a protocol, not a framework:
| Aspect | Framework-specific | MCP |
|---|---|---|
| Learning curve | Different for each | Learn once |
| Tool reusability | None | 100% |
| Switching costs | High | Zero |
| Future-proof | Low | High |
Common Mistakes I Made
Mistake 1: Starting with Frameworks First
I spent months learning frameworks before understanding how tool calling actually works:
# I memorized LangChain patternsfrom langchain.tools import Toolfrom langchain.agents import initialize_agent
# Without understanding:# - What happens when the LLM calls a tool?# - How does the framework serialize inputs?# - What's the actual protocol?Then when I tried a different framework, I was lost.
Mistake 2: Ignoring the Protocol Layer
I treated tool integration as a “solved problem” provided by frameworks:
My assumption: "The framework handles tool calling, I just write functions"
Reality: Understanding the protocol helps debug issues:- Why did my tool fail?- What did the LLM actually send?- How do I add retries?Mistake 3: Learning Platform-Specific Integration
I learned OpenAI’s function calling syntax:
functions = [{ "name": "get_weather", "description": "Get weather for location", "parameters": { "type": "object", "properties": { "location": {"type": "string"} } }}]Then I had to learn Claude’s tool use syntax, then Gemini’s. MCP unifies these.
How MCP Works Under the Hood
The protocol uses JSON-RPC 2.0:
┌──────────────┐ ┌──────────────┐│ MCP Client │ │ MCP Server ││ (Claude/etc) │ │ (Your tools) │└──────┬───────┘ └──────┬───────┘ │ │ │ initialize │ │ ─────────────────────────────────>│ │ │ │ tools/list │ │ ─────────────────────────────────>│ │ │ │ [Tool definitions] │ │ <─────────────────────────────────│ │ │ │ tools/call (create_user) │ │ ─────────────────────────────────>│ │ │ │ [Tool result] │ │ <─────────────────────────────────│ │ │The client and server communicate over stdio, HTTP, or any transport:
from mcp.server.stdio import stdio_server
async def main(): async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, app.create_initialization_options() )This simple protocol means I can debug with raw JSON:
{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "create_user", "arguments": { "name": "Test User" } }, "id": 1}Building My First MCP Server
Here’s a complete example I built for database operations:
import osfrom mcp.server import Serverfrom mcp.types import Tool, TextContent, Resourcefrom mcp.server.stdio import stdio_serverimport asyncpg
app = Server("database-tools")pool = None
@app.initialize()async def initialize(): global pool pool = await asyncpg.create_pool(os.environ["DATABASE_URL"])
@app.list_resources()async def list_resources(): return [ Resource( uri="table:///users", name="Users Table", mimeType="application/json" ), Resource( uri="table:///orders", name="Orders Table", mimeType="application/json" ) ]
@app.read_resource()async def read_resource(uri: str): if uri.startswith("table:///"): table = uri.replace("table:///", "") rows = await pool.fetch(f"SELECT * FROM {table} LIMIT 100") return [dict(row) for row in rows]
@app.list_tools()async def list_tools(): return [ Tool( name="query_database", description="Run a SQL query (SELECT only)", inputSchema={ "type": "object", "properties": { "sql": { "type": "string", "description": "SELECT query to execute" } }, "required": ["sql"] } ), Tool( name="insert_row", description="Insert a row into a table", inputSchema={ "type": "object", "properties": { "table": {"type": "string"}, "data": {"type": "object"} }, "required": ["table", "data"] } ) ]
@app.call_tool()async def call_tool(name: str, arguments: dict): if name == "query_database": sql = arguments["sql"] if not sql.strip().upper().startswith("SELECT"): return [TextContent( type="text", text="Error: Only SELECT queries allowed" )] rows = await pool.fetch(sql) return [TextContent( type="text", text=str([dict(r) for r in rows]) )]
elif name == "insert_row": table = arguments["table"] data = arguments["data"] columns = ", ".join(data.keys()) values = ", ".join(f"${i+1}" for i in range(len(data))) await pool.execute( f"INSERT INTO {table} ({columns}) VALUES ({values})", *data.values() ) return [TextContent( type="text", text=f"Inserted into {table}" )]
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__": import asyncio asyncio.run(main())I tested it:
# Install MCP SDKpip install mcp
# Run the serverDATABASE_URL=postgresql://localhost/mydb python db_mcp_server.py
# Server starts and waits for client connection via stdioWhy Learn MCP Early
Deep_Ad1959 on Reddit said: “Skip the framework rabbit hole at first. Learn how to talk to an LLM via API, understand tokens and context windows, then build something small end to end. The frameworks change every month but understanding how tool use and function calling actually work under the hood transfers everywhere.”
This is why MCP matters:
- Industry momentum - Anthropic, OpenAI, and others are adopting MCP
- Future-proof skills - Protocol knowledge transfers across frameworks
- Composable architecture - Build tools once, use everywhere
- Debug capability - Understand what’s happening under the hood
- Less churn - Standards change slower than frameworks
Summary
In this post, I explained MCP (Model Context Protocol), an open standard for connecting AI models to external tools and data. The key point is that MCP provides three primitives - Resources for reading data, Tools for executing actions, and Prompts for reusable templates - that work across any MCP-compatible AI system.
I showed why MCP matters: it solves framework churn, vendor lock-in, and integration complexity. Learning MCP early means your skills transfer across frameworks, and you understand tool calling at the protocol level rather than just the framework level.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Model Context Protocol Official Documentation
- 👨💻 MCP GitHub Organization
- 👨💻 Anthropic MCP Announcement
- 👨💻 Reddit Discussion: Getting Started with AI Engineering
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments