The Model Context Protocol (MCP) is the standard way AI agents connect to external tools and data sources. Every major AI platform β Claude, ChatGPT, Cursor, Windsurf β now supports MCP. If you're building AI-powered tools, you need to know how to build MCP servers.
In this guide, you'll build a working MCP server from scratch using Python. No prior MCP experience needed.
MCP is a protocol that lets AI models interact with external tools, resources, and prompts. Think of it as a USB port for AI β any tool that speaks MCP can be plugged into any AI that supports MCP.
An MCP server exposes:
mkdir my-first-mcp-server
cd my-first-mcp-server
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install mcp
Create the project structure:
my-first-mcp-server/
βββ src/
β βββ my_server/
β βββ __init__.py
β βββ __main__.py
β βββ server.py
βββ requirements.txt
βββ README.md
Create src/my_server/server.py:
from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.stdio
server = Server("my-first-server")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="hello",
description="Say hello to someone",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The person's name"
}
},
"required": ["name"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "hello":
return [TextContent(
type="text",
text=f"Hello, {arguments['name']}! π Welcome to MCP."
)]
raise ValueError(f"Unknown tool: {name}")
Create src/my_server/__main__.py:
import asyncio
from .server import server, mcp
async def main():
async with mcp.server.stdio.stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
Let's add a tool that does something useful β generate a timestamp:
from datetime import datetime, timezone
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="hello",
description="Say hello to someone",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name"}
},
"required": ["name"]
}
),
Tool(
name="timestamp",
description="Get the current timestamp in various formats",
inputSchema={
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "Format: 'iso', 'unix', or 'readable'",
"enum": ["iso", "unix", "readable"]
}
},
"required": ["format"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "hello":
return [TextContent(type="text", text=f"Hello, {arguments['name']}! π")]
if name == "timestamp":
now = datetime.now(timezone.utc)
fmt = arguments["format"]
if fmt == "iso":
result = now.isoformat()
elif fmt == "unix":
result = str(int(now.timestamp()))
else:
result = now.strftime("%B %d, %Y at %I:%M %p UTC")
return [TextContent(type="text", text=result)]
raise ValueError(f"Unknown tool: {name}")
Use the MCP Inspector to test:
npx @modelcontextprotocol/inspector python -m my_server
This opens a web UI where you can:
Add to your Claude Code settings (~/.claude/settings.json):
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["-m", "my_server"],
"cwd": "/path/to/my-first-mcp-server"
}
}
}
Now ask Claude: "What tools do you have?" and it will list your custom tools!
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ src/
CMD ["python", "-m", "my_server"]
Best for local development and Claude Code integration.
For cloud deployment, use Server-Sent Events:
# In your server.py
from mcp.server.sse import SseServerTransport
sse = SseServerTransport("/messages")
app = Starlette(routes=[sse.get_sse_endpoint(), ...])
Now that you can build MCP servers, here are ideas for your next project: