MCP
6 / 6
MCP 3 min

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.

python
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.

python
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.

python
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`:

json
{
  "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.

Ship One Useful Tool First

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.

Module Complete
You now know why MCP exists, how its three layers talk to each other, what it can expose, how transport and security actually work, and you've built and connected a real server. Next module: Agentic AI, where tools like these get wired into systems that decide for themselves when and how to use them.