Building Your First MCP Server
Use the Python MCP SDK to create a server that exposes resources, tools, and prompts, then connect it to Claude Desktop to see it working end to end.
Building Your First MCP Server is a free AI Engineering Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Project Setup for an MCP Server
Building an MCP server in Python requires the mcp SDK and a Python environment. You'll create a server that Claude Desktop or any MCP client can connect to via stdio. Start by installing the package and creating your server file.
# Create a project directory
# mkdir my_mcp_server && cd my_mcp_server
# Create a virtual environment
# python -m venv venv && source venv/bin/activate
# Install the MCP SDK
# pip install mcp httpx
# Project structure:
# my_mcp_server/
# server.py <- Your MCP server
# requirements.txt
# README.mdCreating the Server Object
Import from the mcp package and create a Server instance with your server's name. The name is shown to clients in their MCP server list — pick something descriptive. The server object is the entry point for registering all your tools, resources, and prompts.
# server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
import asyncio
import httpx
# Create the server — name is shown in Claude Desktop
app = Server('weather-server')
# --- Tool registrations go here ---
# Entry point
if __name__ == '__main__':
asyncio.run(stdio_server(app))Registering Tools with @app.list_tools()
The @app.list_tools() decorator registers a handler that returns the list of available tools when the client asks. Each tool is defined with a name, a description, and an inputSchema — the JSON Schema describing its parameters. The client sends this list to the LLM so it knows which tools it can call.
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name='get_weather',
description='Get current weather for a city. Use when the user asks about weather in a specific location.',
inputSchema={
'type': 'object',
'properties': {
'city': {
'type': 'string',
'description': 'City name, e.g. London or New York'
},
'units': {
'type': 'string',
'enum': ['metric', 'imperial'],
'description': 'Temperature units. Default is metric.'
}
},
'required': ['city']
}
),
types.Tool(
name='list_cities',
description='Return a list of major cities the user can query weather for.',
inputSchema={'type': 'object', 'properties': {}, 'required': []}
)
]Implementing Tool Execution
The @app.call_tool() decorator handles tool execution. When the client calls a tool, this handler receives the tool name and arguments. Execute the appropriate logic and return a list of TextContent objects containing the result string.
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == 'list_cities':
cities = ['London', 'New York', 'Tokyo', 'Paris', 'Sydney']
return [types.TextContent(type='text', text=', '.join(cities))]
if name == 'get_weather':
city = arguments['city']
units = arguments.get('units', 'metric')
unit_symbol = 'C' if units == 'metric' else 'F'
# Call real weather API (stub here)
async with httpx.AsyncClient() as client:
# Replace with actual API call
result = f'{city}: 18{chr(176)}{unit_symbol}, partly cloudy, humidity 65%'
return [types.TextContent(type='text', text=result)]
raise ValueError(f'Unknown tool: {name}')Exposing Resources
Resources are static or dynamic data the model can read — think of them like files. Define resource URIs and implement a resource reader. Resources appear in the client as items the AI can reference, useful for configuration, documentation, or frequently-accessed data snapshots.
@app.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri='weather://supported-cities',
name='Supported Cities List',
description='Complete list of cities available in this weather server.',
mimeType='text/plain'
)
]
@app.read_resource()
async def read_resource(uri: str) -> str:
if uri == 'weather://supported-cities':
cities = ['London', 'New York', 'Tokyo', 'Paris', 'Sydney', 'Dubai', 'Singapore']
return '\n'.join(cities)
raise ValueError(f'Unknown resource URI: {uri}')Registering Prompt Templates
Prompts are reusable message templates that clients surface to users as slash commands or quick actions. They accept parameters and return a list of messages that become the initial context for a conversation. Prompts are great for encoding complex instructions users trigger with a single command.
@app.list_prompts()
async def list_prompts() -> list[types.Prompt]:
return [
types.Prompt(
name='weather-report',
description='Generate a formatted weather report for a city.',
arguments=[
types.PromptArgument(name='city', description='City name', required=True)
]
)
]
@app.get_prompt()
async def get_prompt(name: str, arguments: dict) -> types.GetPromptResult:
if name == 'weather-report':
city = arguments.get('city', 'London')
return types.GetPromptResult(
description=f'Weather report for {city}',
messages=[
types.PromptMessage(
role='user',
content=types.TextContent(
type='text',
text=f'Use the get_weather tool to look up {city} and give me a detailed weather report including what clothing I should wear.'
)
)
]
)
raise ValueError(f'Unknown prompt: {name}')Connecting to Claude Desktop
To use your MCP server with Claude Desktop, add it to the Claude Desktop configuration file. On macOS, this is at ~/Library/Application Support/Claude/claude_desktop_config.json. Specify the command to start your server and any environment variables it needs.
# ~/Library/Application Support/Claude/claude_desktop_config.json
# Add this JSON configuration:
# {
# "mcpServers": {
# "weather-server": {
# "command": "/path/to/venv/bin/python",
# "args": ["/path/to/my_mcp_server/server.py"],
# "env": {
# "WEATHER_API_KEY": "your_api_key_here"
# }
# }
# }
# }
# After saving, restart Claude Desktop.
# Your server's tools will appear in Claude's tool list.Testing Your Server with the MCP CLI
Before connecting to Claude Desktop, test your server using the MCP inspector or CLI tools. The mcp dev command starts your server and opens a browser-based inspector where you can call tools manually and see raw protocol messages, making it easy to debug issues.
# Install the MCP development tools
# pip install 'mcp[cli]'
# Start the inspector with your server
# mcp dev server.py
# The inspector opens at http://localhost:5173
# You can:
# - See all registered tools and their schemas
# - Call tools with custom arguments
# - Browse available resources
# - Test prompt templates
# - View the full JSON-RPC message exchangeError Handling in MCP Servers
MCP servers should never crash on bad input. Wrap all tool execution in try/except and return error messages as TextContent rather than raising exceptions. For fatal errors (configuration issues, missing API keys), log them on startup and raise before the server enters its main loop.
@app.call_tool()
async def call_tool_safe(name: str, arguments: dict) -> list[types.TextContent]:
try:
if name == 'get_weather':
city = arguments.get('city')
if not city:
return [types.TextContent(type='text', text='Error: city argument is required.')]
result = await fetch_weather(city, arguments.get('units', 'metric'))
return [types.TextContent(type='text', text=result)]
raise ValueError(f'Unknown tool: {name}')
except httpx.TimeoutException:
return [types.TextContent(type='text', text='Error: Weather API timed out. Try again.')]
except Exception as e:
return [types.TextContent(type='text', text=f'Error: {str(e)}')]Logging for MCP Servers
Since MCP servers communicate over stdio with the client, print statements will break the protocol. Always use stderr for logging — it goes to a separate stream that doesn't interfere with the MCP message exchange. Configure Python's logging module to write to stderr.
import logging
import sys
# Configure logging to stderr (NOT stdout — that's the MCP channel)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
stream=sys.stderr
)
logger = logging.getLogger('weather-server')
# In your tool handler:
# logger.info(f'Getting weather for {city}')
# logger.error(f'API call failed: {e}')
# Claude Desktop captures stderr to a log file:
# ~/Library/Logs/Claude/mcp-server-weather-server.logPackaging Your MCP Server
Share your MCP server by packaging it with a pyproject.toml and publishing to PyPI, or distribute it as a Docker container for teams. Use environment variables for all secrets — API keys, database URLs — so the server configuration stays separate from the code. Document the required env vars and example config in a clear README.
Quick Check
Test your understanding of building an MCP server in Python.
Lesson Recap
In this lesson you learned: MCP servers expose tools via @app.list_tools() and @app.call_tool() decorators, resources and prompts extend the server with readable data and reusable templates, and logging must use stderr to avoid corrupting the stdio MCP protocol channel. Next up we connect an MCP server to a database and expose dynamic resources with pagination.
Frequently asked questions
Is the “Building Your First MCP Server” lesson free?
Yes — the full text of “Building Your First MCP Server” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.
What will I learn in “Building Your First MCP Server”?
Use the Python MCP SDK to create a server that exposes resources, tools, and prompts, then connect it to Claude Desktop to see it working end to end. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Engineering Academy?
No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Building Your First MCP Server” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Engineering Academy lesson?
Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- What Is MCP and Why It Matters
- Building Your First MCP Server
- Exposing Database Resources via MCP
- MCP Security and Authentication