MCP β Model Context Protocol
- βExplain what MCP is and the problem it solves
- βDescribe the Host/Client/Server architecture
- βExplain the 3 primitives: Tools, Resources, Prompts
- βBuild a working MCP server in Python
- βConnect it to Claude Desktop
The Problem MCP Solves
Before November 2024, every AI tool integration was bespoke. If you wanted Claude to read your database, you wrote custom code. If you wanted VS Code's AI assistant to read your database, you wrote different custom code. If you wanted a third application to read your database, you wrote it a third time. Every integration was a snowflake β unique, non-transferable, and expensive to maintain.
This is the NΓM problem. With N AI applications and M tools, you need NΓM integrations. Every new application multiplies the work.
The Model Context Protocol (MCP) solves this the same way REST standardized HTTP APIs and USB-C standardized charging: by defining a single protocol that both sides implement once. With MCP, you write your tool integration once as an MCP server, and every MCP-compatible host β Claude Desktop, VS Code, Cursor, your custom agent β can use it immediately with zero additional integration work.
The result: N+M integrations instead of NΓM. Write a weather MCP server once. It works everywhere.
MCP was open-sourced by Anthropic in November 2024 and has been adopted by Microsoft, Google, AWS, and hundreds of developers. Over 1,000 MCP servers now exist covering databases, APIs, development tools, cloud services, and local file systems. Every major IDE and AI platform is building MCP support.
MCP standardizes four things: how hosts discover what a server provides, how they call tools, how they read resources, and how they invoke prompt templates. Everything else β the actual capabilities β is up to the server author. This makes MCP both universal and infinitely extensible.
MCP Architecture β Host, Client, Server
MCP defines three roles that are always present in any integration:
HOST APPLICATION (Claude Desktop, VS Code, custom app) βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β ββββββββββββββββ ββββββββββββββββββββββββββ β β β LLM (Claude) βββββββ MCP CLIENT β β β ββββββββββββββββ β (inside the host) β β β β β’ JSON-RPC transport β β β β β’ capability negotiationβ β β β β’ tool call routing β β β ββββββββββββ¬ββββββββββββββ β ββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββ β JSON-RPC over β stdio / HTTP+SSE βββββββββββββΌβββββββββββββββ β MCP SERVER (your code) β β β β π§ Tools β β π Resources β β π¬ Prompts β ββββββββββββββββββββββββββββ
Host is the application embedding Claude β Claude Desktop, VS Code with a Copilot extension, your custom Python script. The host controls the user experience and decides which MCP servers to connect to.
Client is the MCP connector that lives inside the host. You never write the client β the host provides it. The client handles the JSON-RPC protocol, connects to your server process, negotiates capabilities, and routes tool calls from the LLM to your server and responses back.
Server is your code. It exposes capabilities (tools, resources, prompts) to any MCP-compatible host. You write this once and it works with every host.
The transport between Client and Server is JSON-RPC 2.0 β a lightweight remote procedure call protocol over JSON. The actual transport layer (how bytes travel) can be stdio, HTTP+SSE, or Streamable HTTP, which we'll cover at the end of this module.
The 3 MCP Primitives
Every MCP server exposes some combination of three primitives:
| Primitive | What it is | Who calls it | Has side effects? | Example |
|-----------|-----------|-------------|-------------------|---------|
| Tool | A callable function | The LLM model | Yes | send_email(), query_database(), write_file() |
| Resource | A readable data source | The LLM model | No | File contents, database schema, API docs |
| Prompt | A reusable template | The user (via host UI) | No | "Summarize this document", "Review this code" |
Tools are what most people think of when they hear "tool use." The model decides to call a tool, passes arguments, and your code executes. Tools can write to databases, call APIs, send emails β anything with a side effect. The model receives the result and continues generating.
Resources are read-only data sources. Think of them as smart files the model can request. A resource might be the contents of a local file, the current state of a database table, or live API documentation. Resources don't cause changes; they inform the model.
Prompts are reusable message templates with parameters. A prompt named code_review might accept a language parameter and a code parameter and expand into a detailed review request. Users invoke prompts directly from the host UI β they're workflow shortcuts, not model-triggered.
Most MCP servers you build will focus on Tools. Resources and Prompts are valuable for richer integrations.
Building Your First MCP Server
Let's build a weather MCP server that implements all three primitives. Install the SDK first:
pip install mcpHere's a complete MCP server:
# weather_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
import json
# Initialize the server with a name
app = Server("weather-server")
# βββββββββββββββββββββββββββββββββββββββββ
# TOOL: get_weather
# βββββββββββββββββββββββββββββββββββββββββ
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="get_weather",
description=(
"Get the current weather for a city. "
"Returns temperature, conditions, humidity, and wind speed. "
"Use this when the user asks about weather in a specific location."
),
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'San Francisco' or 'London'",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Defaults to celsius.",
"default": "celsius",
},
},
"required": ["city"],
},
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "get_weather":
city = arguments["city"]
unit = arguments.get("unit", "celsius")
# In a real server, call a weather API here
# For this example, we return mock data
temp = 22 if unit == "celsius" else 72
weather_data = {
"city": city,
"temperature": temp,
"unit": unit,
"conditions": "Partly cloudy",
"humidity": "65%",
"wind_speed": "12 km/h",
}
return [
types.TextContent(
type="text",
text=json.dumps(weather_data, indent=2),
)
]
raise ValueError(f"Unknown tool: {name}")
# βββββββββββββββββββββββββββββββββββββββββ
# RESOURCE: popular cities list
# βββββββββββββββββββββββββββββββββββββββββ
@app.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri="weather://cities/popular",
name="Popular Cities",
description="A list of the most commonly queried cities for weather data.",
mimeType="application/json",
)
]
@app.read_resource()
async def read_resource(uri: str) -> str:
if uri == "weather://cities/popular":
cities = [
"New York", "London", "Tokyo", "Paris",
"Sydney", "Toronto", "Berlin", "Singapore",
]
return json.dumps({"cities": cities}, indent=2)
raise ValueError(f"Unknown resource: {uri}")
# βββββββββββββββββββββββββββββββββββββββββ
# PROMPT: weather_report template
# βββββββββββββββββββββββββββββββββββββββββ
@app.list_prompts()
async def list_prompts() -> list[types.Prompt]:
return [
types.Prompt(
name="weather_report",
description="Generate a detailed weather report for a city.",
arguments=[
types.PromptArgument(
name="city",
description="The city to report on",
required=True,
)
],
)
]
@app.get_prompt()
async def get_prompt(name: str, arguments: dict) -> types.GetPromptResult:
if name == "weather_report":
city = arguments.get("city", "unknown city")
return types.GetPromptResult(
description=f"Weather report for {city}",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=(
f"Please get the current weather for {city} and write "
f"a concise, friendly weather report suitable for a "
f"morning news broadcast. Include temperature, conditions, "
f"and any notable weather events."
),
),
)
],
)
raise ValueError(f"Unknown prompt: {name}")
# βββββββββββββββββββββββββββββββββββββββββ
# Run the server over stdio
# βββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
import asyncio
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options(),
)
asyncio.run(main())Let's walk through what this code does:
Server("weather-server")creates the server with a name that hosts display in their UI.@app.list_tools()registers the handler that tells clients what tools exist. Each tool has aname,description, andinputSchemaβ a standard JSON Schema object.@app.call_tool()handles actual tool execution. Validatename, processarguments, returnTextContent.@app.list_resources()and@app.read_resource()implement the Resource primitive.@app.list_prompts()and@app.get_prompt()implement the Prompt primitive.stdio_server()runs the server over stdin/stdout, which is how Claude Desktop launches local MCP servers.
Connecting to Claude Desktop
Once your server is written, connecting it to Claude Desktop takes one config edit. Open or create:
~/Library/Application Support/Claude/claude_desktop_config.json
Add your server under mcpServers:
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/absolute/path/to/weather_server.py"]
}
}
}Quit and relaunch Claude Desktop. You should see a hammer icon in the chat interface indicating MCP tools are available. Ask Claude "What's the weather in Tokyo?" and it will call your get_weather tool automatically.
Write the server once β any MCP-compatible host can use it. The same weather server you connect to Claude Desktop will also work in VS Code Copilot, Cursor, and any custom agent you build with the Anthropic SDK. That's the value of a standard protocol.
For Windows users, the config path is:
%APPDATA%\Claude\claude_desktop_config.json
If your server uses a virtual environment, point to the Python binary inside it:
{
"mcpServers": {
"weather": {
"command": "/path/to/your/venv/bin/python",
"args": ["/absolute/path/to/weather_server.py"]
}
}
}Debugging MCP Servers
Claude Desktop writes MCP logs to:
# macOS
~/Library/Logs/Claude/mcp*.log
# Tail in real time
tail -f ~/Library/Logs/Claude/mcp-server-weather.logYou can also test your server directly without Claude Desktop using the mcp CLI:
pip install mcp[cli]
# List what your server exposes
mcp dev weather_server.py
# Call a specific tool
echo '{"city": "London"}' | mcp call weather_server.py get_weatherCommon startup failures: wrong Python path, missing dependencies in the environment Claude uses, syntax errors (server exits immediately), or incorrect JSON in the config file. Always check the log file first.
Transport Layers
MCP supports three transport options:
stdio (local subprocess, simplest): Claude Desktop launches your server as a child process and communicates over stdin/stdout. Zero network configuration. Perfect for local tools. This is what the example above uses.
HTTP + SSE (remote, shared): Your server runs as an HTTP service. Multiple clients can connect simultaneously. Good for shared team tools or servers that need to run continuously.
Streamable HTTP (2025 spec, preferred for new remote servers): Replaces HTTP+SSE with a cleaner bidirectional streaming model. If you're building a new remote MCP server in 2025, use this.
For learning and local development, stick with stdio. It's simpler, requires no network config, and is what every tutorial example uses.
Security Considerations
MCP servers run with your user permissions. A malicious or buggy MCP server can read your files, make network requests, and execute code. Treat this like you treat browser extensions β only connect to servers you trust.
MCP servers run with your permissions. Only connect to MCP servers you trust. Before running a third-party server: read the code, check what filesystem access it requests, verify it doesn't phone home. A server that lists read_resource on file:///** can read your entire home directory.
When building your own servers:
- Validate all inputs β treat arguments like untrusted user input even though they come from the model.
- Scope filesystem access β if your tool reads files, restrict it to a specific directory, not
/. - Never expose credentials in resources β a resource the model can read should never contain API keys or passwords.
- Rate limit destructive operations β prevent a runaway agent from calling
delete_file10,000 times. - Require confirmation for irreversible actions β return a "confirmation required" response before actually deleting or sending.
Lab 1 β Read an existing server
Pick any MCP server from the official list at github.com/modelcontextprotocol/servers. Read its source. Identify: How many tools does it expose? Does it have resources? What does its inputSchema look like? What would happen if you passed invalid input?
Lab 2 β Build the weather server
Copy the weather server from this module and make it real. Replace the mock data with a call to the Open-Meteo API β it's free and requires no API key. Test that get_weather("Paris", "celsius") returns real data.
Lab 3 β Connect to Claude Desktop
Connect your weather server to Claude Desktop following the config instructions above. Open a new conversation and ask: "What's the weather like in Tokyo right now? Is it a good day for sightseeing?" Observe the tool call in the UI.
Lab 4 β Add a second tool
Add a get_forecast tool to your server that accepts city and days (1β7) and returns a multi-day forecast. Update the list_tools handler. Reconnect to Claude Desktop and ask for a 5-day forecast. Verify both tools appear in the hammer menu.
Q1What is the key difference between an MCP Tool and an MCP Resource?
Q2Which file do you edit to add an MCP server to Claude Desktop?
Q3In MCP's architecture, what is the 'Client'?