How to Build Your First MCP Server: A Step-by-Step Tutorial

Reading about MCP is one thing; building a working server is another. This walks through the minimum real version: a Python MCP server exposing one useful tool, run locally, and connected to an MCP-compatible AI application. No prior MCP experience assumed — if you haven’t read the conceptual overview first, start there for the host/client/server model this builds on.

What You’ll Need

  • Python 3.10 or newer
  • The official MCP Python SDK (pip install mcp)
  • An MCP-compatible client to test with (Claude Desktop, or an editor with MCP support)

Step 1: Define the Server and a Tool

An MCP server’s core job is exposing tools an AI host can call. Here’s a minimal server exposing one tool — checking word count of a piece of text:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("text-tools")

@mcp.tool()
def word_count(text: str) -> int:
    """Count the number of words in a piece of text."""
    return len(text.split())

if __name__ == "__main__":
    mcp.run()

The @mcp.tool() decorator does the real work: it registers word_count as a callable tool, using the function’s docstring as the description the AI host sees when deciding whether to call it. This is the whole pattern — every additional tool is just another decorated function.

Step 2: Add a Second, More Realistic Tool

A single-tool server proves the concept but isn’t useful yet. A more realistic example — reading a local file’s contents, scoped to one safe directory:

import os

ALLOWED_DIR = os.path.expanduser("~/mcp-files")

@mcp.tool()
def read_file(filename: str) -> str:
    """Read the contents of a file from the allowed directory."""
    path = os.path.join(ALLOWED_DIR, filename)
    if not os.path.abspath(path).startswith(os.path.abspath(ALLOWED_DIR)):
        raise ValueError("Access outside the allowed directory is not permitted.")
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

Notice the path check before opening anything — this matters more than it looks like it does. An AI host will call your tool with whatever arguments the model decides to pass, so a server that trusts input blindly (allowing ../../ style paths, for example) is a real vulnerability, not a hypothetical one. Scope every tool’s file/database/API access as narrowly as the task actually requires.

Step 3: Connect It to an MCP Host

Most MCP hosts (Claude Desktop included) read a config file listing which MCP servers to launch and connect to. A typical entry looks like this:

{
  "mcpServers": {
    "text-tools": {
      "command": "python",
      "args": ["/full/path/to/your_server.py"]
    }
  }
}

Restart the host application after adding this, and it should discover both tools automatically — no separate registration step. Ask it something that requires the tool (“how many words are in this paragraph?”) and it should call word_count on its own.

Step 4: Test It Actually Works

Don’t just trust that it’s wired up correctly — ask the host a question that specifically requires the tool, not something it could plausibly guess without calling it, and confirm the returned value is actually correct (not just plausible-sounding). For the file-reading tool, put a file with known, distinctive content in the allowed directory and ask the host to summarize it — if the summary reflects the file’s real content, the tool call genuinely happened.

Frequently Asked Questions

Does an MCP server need to be written in Python?
No — official SDKs exist for Python, TypeScript, and other languages. The protocol itself is language-agnostic; pick whichever SDK matches your existing stack.

Can one MCP server expose more than one tool?
Yes, and it usually should — group related tools (like several file or database operations) into one server rather than a separate server per tool, unless there’s a real reason to isolate them (different permission levels, for example).

Is it safe to connect an AI host to a server with real file or API access?
Only with real scoping and guardrails — least-privilege access, path/input validation like the example above, and human approval for anything destructive or irreversible. Treat any input your tool receives as untrusted, since it’s the model’s interpretation of a user request, not a verified command.

Conclusion

The whole pattern is smaller than it looks from the outside: decorate a function, describe what it does, scope its access narrowly, and any MCP-compatible host can use it. The part that actually takes care is the scoping and validation, not the protocol plumbing — that’s exactly where a server should not be trusted blindly, which the next guide in this series covers in more depth.

📑 About the author: I also build Digital Bizz Card — hosted digital business cards you can share with a QR code, no app required.

Translate ยป
Scroll to Top