ASK KNOX
beta
LESSON 341

Building a Tiny MCP Server of Your Own

A minimal MCP server is under 30 lines of Python — strip away the mystique and what remains is a pattern you could write in an afternoon.

7 min read·MCP for Beginners: Giving Your AI Hands

Introduction

Every MCP server you use was written by someone who sat down, opened an editor, and typed out the code. There is nothing magical about the server side of this equation. Understanding the inside of a server — even a trivially small one — makes you a dramatically more effective consumer of them.

This lesson builds the smallest useful MCP server: one tool, one file, fully functional. The goal is demystification. If you understand this, you understand every server in the ecosystem.

Core Concept

What an MCP Server Actually Does

An MCP server is a process that:

  1. Starts up and announces its capabilities (tools, resources, prompts) to the client
  2. Waits for requests from the client
  3. When a request arrives, calls the relevant function with the provided arguments
  4. Returns the result to the client

That's it. The protocol defines the message format for each of those steps. Libraries like FastMCP handle the protocol layer so you can focus on step 3 — writing the function that actually does the work.

The FastMCP Pattern

FastMCP is a Python library that makes building MCP servers nearly trivial. It follows a decorator-based pattern you have seen in web frameworks like Flask or FastAPI — you write a regular Python function and decorate it to declare it as a tool.

The library handles:

  • Generating the JSON schema description of your tool (from your type hints)
  • Speaking the MCP protocol over stdio
  • Routing incoming requests to the right function
  • Serializing and deserializing arguments and return values

You handle: writing the function.

Practical Application

Here is a complete, working MCP server in one file. It exposes a single tool: get_word_count, which counts words in a string.

# word_count_server.py
from mcp.server.fastmcp import FastMCP

# Initialize the server with a name
server = FastMCP("word-counter")

@server.tool()
def get_word_count(text: str) -> dict:
    """
    Count the number of words in the provided text.
    
    Args:
        text: The text to count words in.
    
    Returns:
        A dict with word_count and character_count.
    """
    words = text.split()
    return {
        "word_count": len(words),
        "character_count": len(text)
    }

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

That is 21 lines. Let's trace through what happens when Claude Code uses it:

Step 1 — Startup: Claude Code reads your config and spawns python word_count_server.py. FastMCP initializes and announces one tool called get_word_count with its schema.

Step 2 — Discovery: Claude Code receives the tool list and now knows it has a get_word_count tool available. It knows the tool takes a text string argument.

Step 3 — Invocation: You ask the AI: "How many words are in my project description?" The AI decides get_word_count is the right tool. It calls get_word_count(text="Your project description here...").

Step 4 — Response: FastMCP calls your Python function, gets back {"word_count": 4, "character_count": 33}, and returns the result to Claude Code. The AI incorporates this into its response.

Wiring It Into Claude Code

{
  "mcpServers": {
    "word-counter": {
      "command": "python",
      "args": ["/path/to/word_count_server.py"]
    }
  }
}

Install the dependency first:

pip install mcp

Restart Claude Code. The get_word_count tool is now available in your session.

Extending to a More Useful Tool

The same pattern scales to real use cases. Here is a tool that reads your own application's local JSON config file — something you might actually want:

@server.tool()
def read_app_config(config_path: str) -> dict:
    """
    Read and return a JSON config file from disk.
    
    Args:
        config_path: Absolute path to the JSON config file.
    
    Returns:
        Parsed config as a dict.
    """
    import json
    from pathlib import Path
    
    path = Path(config_path)
    if not path.exists():
        return {"error": f"File not found: {config_path}"}
    if not path.suffix == ".json":
        return {"error": "Only JSON files are supported"}
    
    return json.loads(path.read_text())

Add it to the same server file with @server.tool() and it becomes a second tool — no additional wiring needed. The server announces both tools on startup.

Real Production Pattern: mcp-bridge

Knox's mcp-bridge server follows this exact pattern at larger scale. It is a FastMCP stdio server (uv run --with fastmcp server.py) that exposes five tools as decorated Python functions, each forwarding calls to the OpenClaw gateway API. The only difference from the example above is that the functions make HTTP requests instead of reading files. The structure is identical.

Common Mistakes

Skipping type hints on tool functions. FastMCP generates the tool's JSON schema from your function's type hints. If you omit them, the schema is vague and the AI cannot reliably pass correct arguments. Always annotate parameters and .

Not writing docstrings. The docstring becomes the tool's description — the text the AI reads to understand what the tool does and when to use it. A missing or vague docstring results in the AI calling your tool at wrong times or not at all.

Returning unserializable types. MCP responses must be serializable to JSON. If your function returns a Python object the SDK can't cleanly convert (like an open file handle or a live database connection), the result may fail or degrade to a lossy string form — some SDKs fall back to str() rather than erroring, which is rarely what you want. For predictable results, return the data itself: dicts, lists, strings, numbers, or booleans.

Building before you need to. Before writing a server, check the public MCP registry (registry.modelcontextprotocol.io) and the official servers directory. Someone may have already built what you need. Building is for gaps — not for reinventing existing servers.

Summary

  • An MCP server is a process that receives tool call requests and returns results — no more complex than a minimal web API
  • FastMCP reduces a working server to a decorated Python function plus a few lines of boilerplate
  • The decorator pattern is identical whether your tool reads a file, calls an API, or queries a database
  • Type hints and docstrings are not optional — they become the tool's schema and description that the AI client uses
  • Real production servers like mcp-bridge use this same FastMCP pattern at scale

What's Next

You have built a server. But you probably want to use it with more than one AI client. The next lesson covers how the same MCP server works across Claude Code, Claude Desktop, and IDE extensions — and what the config differences look like per client.