Model Context Protocol explained through practical Q&A
Table of Contents
- The Problem MCP Solves
- Building Your First MCP Server
- The Client Side
- The Two-Terminal Misconception
- The Three Components
- Client vs Host - The Key Difference
- Why Three Components?
- Testing Your MCP Server
- Real-World Example: GitHub MCP Server
- MCP Transport: stdio vs SSE
- Common Patterns
- Debugging Tips
- Architecture Deep Dive
- Quick Start Summary
- Explore More MCP Servers
1. The Problem MCP Solves π―
Rahul (Developer): Man, I keep hearing about MCP everywhere. What problem does it actually solve?
Amit (Expert): Good question! Let me give you a real-world scenario. Before MCP, if you wanted Claude Desktop to access your GitHub repos, AWS resources, and company database, you'd need:
- Claude team to build custom GitHub integration
- Claude team to build custom AWS integration
- Claude team to build custom database integration
Now multiply this by every AI app (VS Code, Cursor, ChatGPT, etc.). That's N apps Γ M tools = NΓM custom integrations!
Rahul: That sounds like a nightmare.
Amit: Exactly! MCP solves this with a standard protocol. Build ONE GitHub MCP server, and it works everywhere - Claude, VS Code, Cursor, ChatGPT. Now it's just N apps + M tools = N+M integrations. Much cleaner!
Rahul: So it's like REST APIs for AI tools?
Amit: Similar concept, but different implementation. Let me show you the simplest MCP example - a math server with add and multiply tools.
2. Building Your First MCP Server π οΈ
Rahul: Alright, show me the code!
Amit: Sure! Here's a complete MCP server in 30 lines:
# mcp_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import asyncio
server = Server("math-server")
@server.list_tools()
async def list_tools():
return [
Tool(name="add", description="Add two numbers", inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
}),
Tool(name="multiply", description="Multiply two numbers", inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
})
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "add":
result = arguments["a"] + arguments["b"]
elif name == "multiply":
result = arguments["a"] * arguments["b"]
return [TextContent(type="text", text=str(result))]
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
Rahul: That's it? Just decorators and async functions?
Amit: Yep! Just two key decorators:
-
@server.list_tools()- Tells clients what tools are available -
@server.call_tool()- Handles tool execution
The MCP SDK handles all the JSON-RPC protocol details for you.
3. The Client Side π
Rahul: How do I use this server?
Amit: Here's a client that connects to the server:
# mcp_client.py
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import asyncio
import sys
async def main():
server_params = StdioServerParameters(
command=sys.executable,
args=["mcp_server.py"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize
await session.initialize()
# List tools
tools = await session.list_tools()
print("Tools:", [t.name for t in tools.tools])
# Call add tool
result = await session.call_tool("add", arguments={"a": 7, "b": 3})
print("7 + 3 =", result.content[0].text)
# Call multiply tool
result = await session.call_tool("multiply", arguments={"a": 4, "b": 6})
print("4 * 6 =", result.content[0].text)
if __name__ == "__main__":
asyncio.run(main())
Rahul: Wait, where do I start the server? Do I run it in another terminal?
Amit: Nope! That's the key difference with MCP. Check out this line:
StdioServerParameters(command=sys.executable, args=["mcp_server.py"])
The client spawns the server as a subprocess. You never manually start the server.
4. The Two-Terminal Misconception β
Rahul: But normally I start the server first, then connect clients. Why can't I do that here?
Amit: Good question! Let me show you why:
Traditional HTTP/TCP:
# Terminal 1
python http_server.py # Listens on port 8000
# Terminal 2
curl http://localhost:8000 # Connects to Terminal 1 β
MCP stdio:
# Terminal 1
python mcp_server.py # Waits on stdin (no port)
# Terminal 2
python mcp_client.py # Spawns its OWN server β
Rahul: So they can't talk to each other?
Amit: Right. MCP uses stdio pipes (stdin/stdout), not network ports. Each terminal has separate stdin/stdout streams. The client spawns the server as a subprocess and communicates via pipes:
Client Process Server Process
| |
|-- subprocess.Popen ------------->| (spawned)
| |
|-- JSON via stdin --------------->|
| |-- execute tool
|<-- JSON via stdout --------------|-- return result
| |
|-- terminate ------------------->| (killed)
Rahul: So the client controls the server's lifecycle?
Amit: Exactly! Here's why that's good:
- No port conflicts
- No manual server management
- Process isolation (each client gets own server)
- Automatic cleanup (server dies when client exits)
5. The Three Components π
Rahul: OK, I see Server and Client. But what's this "Host" everyone talks about?
Amit: Let me break down the three roles:
- Server - Provides tools (your math server)
- Client - MCP protocol handler (connects to server)
- Host - Application that uses servers (Claude Desktop, VS Code, AI agents)
Rahul: Got an example?
Amit: Sure! Here's an AI agent (Host) using our math server:
# mcp_host.py
from mcp import stdio_client, StdioServerParameters
from strands import Agent
from strands.tools.mcp import MCPClient
import sys
# Host application that uses MCP server
mcp_client = MCPClient(lambda: stdio_client(
StdioServerParameters(
command=sys.executable,
args=["mcp_server.py"]
)
))
with mcp_client:
tools = mcp_client.list_tools_sync()
print("Available tools:", len(tools), "tools")
agent = Agent(tools=tools)
response = agent("What is 15 + 27? Then multiply that result by 3")
print(response)
6. Client vs Host - The Key Difference π
Rahul: Hold on! What's the difference between Client and Host? They look the same to me. Both connect to the server, right?
Amit: I know, they do look similar! But they serve different purposes. Let me show you:
Client (mcp_client.py) - Low-level MCP protocol:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
async with stdio_client(StdioServerParameters(...)) as (read, write):
async with ClientSession(read, write) as session:
# You manually call each tool
result = await session.call_tool("add", {"a": 7, "b": 3})
print(result.content[0].text) # Output: 10
Host (mcp_host.py) - Application using MCP:
from strands import Agent
from strands.tools.mcp import MCPClient
mcp_client = MCPClient(lambda: stdio_client(...))
with mcp_client:
tools = mcp_client.list_tools_sync()
agent = Agent(tools=tools)
# AI agent decides which tools to call
response = agent("What is 15 + 27? Then multiply that result by 3")
# Agent automatically: add(15,27) β multiply(42,3) β answer
Rahul: I'm still not seeing it. Both use MCP client, right?
Amit: Yeah, but check out how they're used:
Client:
- Direct MCP protocol usage
- You explicitly call each tool
- You control the flow
- Good for: Testing, debugging, simple scripts
Host:
- Application built on top of MCP
- AI/logic decides which tools to call
- Autonomous decision-making
- Good for: AI assistants, complex workflows
Rahul: Can you show them side-by-side?
Amit: Absolutely!
Scenario: Calculate (10 + 5) * 3
Client approach:
# You write the logic
result1 = await session.call_tool("add", {"a": 10, "b": 5})
intermediate = int(result1.content[0].text) # 15
result2 = await session.call_tool("multiply", {"a": intermediate, "b": 3})
final = result2.content[0].text # 45
Host approach:
# Agent figures out the logic
response = agent("Calculate 10 + 5, then multiply by 3")
# Agent automatically:
# 1. Calls add(10, 5) β 15
# 2. Calls multiply(15, 3) β 45
# 3. Returns answer
Rahul: Ah! So Client is like using a library directly, and Host is building an app with that library?
Amit: Exactly! Here's another way to think about it:
Client = MCP Protocol Handler
- Raw access to MCP servers
- Manual tool invocation
- You write the orchestration logic
Host = Application Using MCP
- Built on top of MCP client
- Autonomous tool usage (AI, rules, workflows)
- Application-specific features (UI, auth, logging)
7. Why Three Components? π€
Rahul: Why not just Server + Client? Why do we need Host?
Amit: Good question! Here's why:
Why not just Server?
- Apps need a standard way to discover and call tools
- Client provides the MCP protocol API
Why not just Server + Client?
- Host manages lifecycle (start/stop servers)
- Host handles multiple servers simultaneously
- Host provides app-specific features (UI, auth, logging)
Real-world example:
Claude Desktop (Host)
β manages
GitHub MCP Server + AWS MCP Server + Database MCP Server
β via
MCP Client (protocol handler)
8. Testing Your MCP Server π§ͺ
Rahul: How do I test my server without writing a client?
Amit: Use MCP Inspector! It's a visual debugging tool that makes life easier:
# Install
npm install -g @modelcontextprotocol/inspector
# Run
mcp-inspector python mcp_server.py
# Open browser
http://localhost:5173
Rahul: What can I do with it?
Amit: Lots of stuff:
- See all available tools
- Test tools with custom arguments
- View JSON-RPC messages
- Debug responses in real-time
For example:
- Click "Tools" tab β see
addandmultiply - Click
addβ enter{"a": 10, "b": 5} - Click "Execute" β see result
15 - View raw JSON-RPC in Logs tab
Rahul: Nice! Way easier than writing test clients!
9. Real-World Example: GitHub MCP Server π
Rahul: You mentioned GitHub earlier. How's that work?
Amit: GitHub MCP Server is just like our math server, but it provides GitHub operations:
# Any app can use it
mcp_client = MCPClient(lambda: stdio_client(
StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-github"]
)
))
# Now you have tools like:
# - create_issue
# - search_repositories
# - read_file
# - etc.
Rahul: So it's stdio transport, not HTTP?
Amit: Correct! It runs as a local subprocess:
Claude Desktop (Host)
β spawns subprocess
GitHub MCP Server (Node.js script)
β provides tools via stdio
Claude can now:
- Create GitHub issues
- Search repositories
- Read file contents
10. MCP Transport: stdio vs SSE π
Rahul: You mentioned stdio transport. Are there other ways to implement MCP?
Amit: Yes! MCP supports two transport mechanisms:
1. stdio (stdin/stdout) - What we've been using
- Local subprocess communication
- No network ports
- Most common for local development
- What we covered in this tutorial
2. SSE (Server-Sent Events) - HTTP-based
- Web-based communication
- Uses HTTP with Server-Sent Events
- For remote/web access
- Can be accessed via localhost URL
Rahul: Can you show me SSE implementation?
Amit: Sure! Here's the same math server using SSE:
Server with SSE:
from mcp.server import Server
from mcp.server.sse import sse_server
from starlette.applications import Starlette
from starlette.routing import Route
import uvicorn
server = Server("math-server")
# Same @server.list_tools() and @server.call_tool() as before
app = Starlette(
routes=[
Route("/sse", endpoint=sse_server(server), methods=["GET", "POST"])
]
)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Client with SSE:
from mcp.client.sse import sse_client
from mcp import ClientSession
import asyncio
async def main():
async with sse_client("http://localhost:8000/sse") as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("add", arguments={"a": 5, "b": 3})
print("5 + 3 =", result.content[0].text)
if __name__ == "__main__":
asyncio.run(main())
Rahul: When should I use each?
Amit: Here's the breakdown:
Use stdio when:
- Building local tools
- Desktop applications
- CLI tools
- Development/testing
- No remote access needed
Use SSE when:
- Web applications
- Remote server access
- Cloud deployments
- Multiple clients from different machines
- Browser-based clients
Rahul: Got it! So stdio for local, SSE for web.
Amit: Exactly! Most MCP servers start with stdio for simplicity, then add SSE if remote access is needed.
11. Common Patterns π
Rahul: What are the typical use cases?
Amit: Here are the common patterns:
1. Data Access:
# Database MCP Server
@server.call_tool()
async def call_tool(name, arguments):
if name == "query_database":
return execute_sql(arguments["query"])
2. API Integration:
# Slack MCP Server
@server.call_tool()
async def call_tool(name, arguments):
if name == "send_message":
return slack_api.post_message(arguments["channel"], arguments["text"])
3. File Operations:
# Filesystem MCP Server
@server.call_tool()
async def call_tool(name, arguments):
if name == "read_file":
return open(arguments["path"]).read()
Rahul: So basically any operation can be an MCP tool?
Amit: Yep! If you can code it, you can expose it via MCP.
12. Debugging Tips π
Rahul: What if my server isn't working?
Amit: Here's a quick verification checklist:
1. Server Startup Check:
python mcp_server.py
- No errors = server code is valid β
- Waits for input = ready to accept connections β
- Ctrl+C to exit
2. Client Test:
python mcp_client.py
Expected output:
Tools: ['add', 'multiply'] β Server responding
7 + 3 = 10 β Tools working
4 * 6 = 24 β All tests passed
3. Inspector Test:
mcp-inspector python mcp_server.py
Visual confirmation of tools and responses.
Common issues:
Server won't start:
pip install mcp # Missing dependency
Tools not showing:
Check @server.list_tools() decorator exists
Tool execution fails:
Check @server.call_tool() handles the tool name
13. Architecture Deep Dive ποΈ
Rahul: Can you walk me through the full architecture one more time?
Amit: Sure! Here's the complete flow:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Host Application (Claude Desktop, VS Code) β
β β
β βββββββββββββββββββββββββββββββββββββββββ β
β β MCP Client (Protocol Handler) β β
β β β β
β β - Spawns server subprocess β β
β β - Manages stdio pipes β β
β β - Sends JSON-RPC requests β β
β β - Receives JSON-RPC responses β β
β βββββββββββββββββ¬ββββββββββββββββββββββββ β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β subprocess.Popen
β stdin/stdout pipes
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β MCP Server (Tool Provider) β
β β
β @server.list_tools() β
β - Returns available tools β
β β
β @server.call_tool() β
β - Executes tool logic β
β - Returns results β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Communication:
- Host spawns server via
subprocess.Popen - Host writes JSON to server's stdin
- Server reads from stdin, processes request
- Server writes JSON to stdout
- Host reads from server's stdout
- Host terminates server when done
Rahul: And this all happens automatically?
Amit: Yep! The MCP SDK handles all the plumbing. You just write:
Server side:
@server.list_tools() # What tools do you have?
@server.call_tool() # Execute this tool
Client side:
session.list_tools() # Get available tools
session.call_tool(name, args) # Call a tool
14. Quick Start Summary β‘
Rahul: Give me the quick version to get started.
Amit: Here you go:
# Install
pip install mcp
# Create server (mcp_server.py)
# - Use @server.list_tools() to register tools
# - Use @server.call_tool() to handle execution
# Create client (mcp_client.py)
# - Use StdioServerParameters to spawn server
# - Use ClientSession to call tools
# Test
python mcp_client.py
# Debug
npm install -g @modelcontextprotocol/inspector
mcp-inspector python mcp_server.py
Rahul: That's it?
Amit: That's it! Now you understand:
- β What MCP solves (N+M vs NΓM integrations)
- β Three components (Server, Client, Host)
- β stdio transport (subprocess, not network)
- β Why not two terminals (pipes, not ports)
- β How to build servers (decorators)
- β How to test (Inspector)
- β Real-world usage (GitHub, databases, APIs)
Rahul: Awesome! Time to build my first MCP server.
Amit: Go for it! Remember: any tool you build will work in every AI app that supports MCP. That's the power of standards.
15. Explore More MCP Servers π
Rahul: This is cool! Are there more MCP servers I can explore?
Amit: Absolutely! Check out these resources:
Official MCP Servers:
- π GitHub - Official MCP Servers
- Maintained by the MCP team
- Production-ready servers
- Great examples to learn from
Community Resources:
-
π mcp.so
- Curated list of MCP servers
- Community contributions
- Search by category
-
π mcpservers.org
- Comprehensive directory
- Server ratings and reviews
- Installation guides
Popular MCP Servers:
- GitHub (create issues, search repos)
- AWS (manage cloud resources)
- PostgreSQL (database queries)
- Filesystem (file operations)
- Slack (send messages)
- Google Drive (file management)
- And many more!
Rahul: Awesome! Time to explore and build.
Amit: Go for it! Remember: any tool you build will work in every AI app that supports MCP. That's the power of standards.
Run the demos:
pip install mcp strands
python mcp_client.py # See client in action
python mcp_host.py # See AI agent using MCP
mcp-inspector python mcp_server.py # Visual debugging
Happy MCP building! π
Top comments (1)
Please give any feedback and share with others if this helps understanding MCP better. ππ