Build Your First MCP Server
Step by step: create a working MCP server in Python that exposes a real, callable tool.
This is the full, real code for `hello-mcp-server` from the companion repo: one tool, no API keys, no external services. It is the exact file you can run right now. We'll build it in three pieces.
Step 1: Declare the server and its one tool. Every tool needs a name, a description the model will read, and a JSON Schema for its arguments. The description quality point from two lessons ago applies here directly.
import asyncio
import re
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import CallToolResult, ListToolsResult, TextContent, Tool
server = Server("hello-mcp-server")
@server.list_tools()
async def list_tools() -> ListToolsResult:
return ListToolsResult(tools=[
Tool(
name="analyze_text",
description=(
"Analyze a block of text and return word count, character "
"count, sentence count, and estimated reading time. Use this "
"whenever the user pastes text and asks how long it is."
),
inputSchema={
"type": "object",
"properties": {"text": {"type": "string", "description": "The text to analyze"}},
"required": ["text"],
},
),
])Step 2: Implement the handler. This is plain Python. No MCP-specific logic here at all, just validate the input and do the work.
WORDS_PER_MINUTE = 200
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
if name != "analyze_text":
return CallToolResult(content=[TextContent(type="text", text=f"Unknown tool: {name}")], isError=True)
text = arguments.get("text", "")
if not text.strip():
return CallToolResult(content=[TextContent(type="text", text="No text provided.")], isError=True)
words = text.split()
word_count = len(words)
sentence_count = len(re.findall(r"[.!?]+", text)) or 1
reading_time_min = max(1, round(word_count / WORDS_PER_MINUTE))
summary = (
f"Words: {word_count}\nCharacters: {len(text)}\n"
f"Sentences: {sentence_count}\nEstimated reading time: {reading_time_min} min"
)
return CallToolResult(content=[TextContent(type="text", text=summary)])Step 3: Run it over stdio. This is the same three lines in every stdio-based MCP server you will ever write.
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())Connect it to a real host. Add this to Claude Desktop's or Cursor's MCP config file, pointing at the absolute path of your `server.py`:
{
"mcpServers": {
"hello-mcp": {
"command": "python",
"args": ["/absolute/path/to/hello-mcp-server/server.py"]
}
}
}Restart the host, and ask it something like 'How long would this paragraph take to read: ...'. If the tool is wired correctly, you will see the host issue the exact `tools/call` message from the MCP Message Inspector two lessons back, and the model's answer will be grounded in the real word count your code computed, not a guess.
Do not build a giant toolbox on day one. Ship one screwdriver that works perfectly, then add the wrench and pliers after you validate real usage. The `github-server` and `postgres-server` in the companion repo both started this small before growing to multiple tools and resources.