Builds·6 min read·Jul 16, 2026

Build Your First MCP Server in 10 Lines

Claude Desktop has no clock, no access to your files, no line to your database. An MCP server is the Python function that changes that, and it fits in about ten lines. This is the full build from the video: the zero-dependency get_current_time server, why uv beats pip (stock macOS ships Python 3.9, mcp needs 3.10+), the Claude Desktop config, and the two silent-failure traps that cost everyone a restart: the absolute paths, and the one print() that corrupts the whole connection.

An MCP server is a Python function that Claude is allowed to call. That is the whole concept. You write a normal function, add one decorator, point Claude Desktop at the file, and Claude can run your code instead of guessing. This builds a working one in about ten lines, wires it into Claude Desktop, and covers the two config mistakes that silently break it.

The problem nobody talks about

Claude Desktop cannot see your machine. It has no clock, no reach into your files, no line to your database. Ask it for the exact time right now and it will guess, because it genuinely has no way to check. That gap is the point. Everything useful you want Claude to do, read your data, hit your API, look at your calendar, lives on the other side of a wall it cannot cross on its own.

There is also the assumption that MCP is some heavy framework you need to sit down and study. It is not. The Model Context Protocol is just the agreed way to hand Claude a tool it can call. FastMCP, which ships inside the official mcp SDK, turns a plain function into one of those tools. The server below needs nothing else and fits on one screen.

Why this is smaller than it looks

The decorator is the entire mechanism. Putting @mcp.tool() above a function is what exposes it to Claude. There is no registry to edit, no route to define.

The docstring is not a comment, it is the interface.FastMCP builds the tool's schema from your type hints and that docstring. Leave it out and Claude has no idea what the tool does or when to reach for it.

There are no dependencies to chase. get_current_time uses datetime from the standard library, so past the SDK itself there is nothing to install.

Step 1: Install uv (and skip pip)

curl -LsSf https://astral.sh/uv/install.sh | sh

Run the server with one line, no project scaffold:

uv run --with mcp server.py

No uv init, no uv add, zero project files. uv pulls mcp in for that run. The reason to use it over pip is not taste. Stock macOS ships Python 3.9, but mcp needs 3.10 or newer, so pip install mcp fails out of the box on most machines with a cryptic error. uv downloads a correct Python and manages the environment for you, so the version landmine never goes off.

One thing that looks wrong but is fine: when you run that line, the terminal just sits there with no output, like it froze. That is a STDIO server waiting silently for Claude to connect over stdin and stdout. Ctrl+C stops it.

Step 2: Write the server

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

mcp = FastMCP("my-first-server")

@mcp.tool()
def get_current_time() -> str:
    """Get the current date and time on this computer."""
    return datetime.now().strftime("%A, %B %d, %Y at %I:%M %p")

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

mcp.run() defaults to STDIO, which is exactly what Claude Desktop connects to. The strftimeformat returns something like "Thursday, July 16, 2026 at 02:30 PM", which reads better on screen than raw ISO. That is the whole server, nothing hidden below the fold.

Step 3: The one line that silently kills it

Never print() to stdout in a STDIO server. The server talks to Claude over stdout using JSON-RPC messages, and a stray print() writes straight into that stream and corrupts it. The server dies with no obvious error, and you will swear the code is fine. If you need to log something, send it to stderr:

import sys
print("checking the time...", file=sys.stderr)

This is the most common beginner mistake and almost nobody warns you about it.

Step 4: Wire it into Claude Desktop

Open Settings, then Developer, then Edit Config. That opens claude_desktop_config.json. Paste this:

{
  "mcpServers": {
    "my-first-server": {
      "command": "/Users/you/.local/bin/uv",
      "args": ["run", "--with", "mcp", "/Users/you/path/to/server.py"]
    }
  }
}

Two gotchas, and both fail silently, meaning the server just never shows up:

  1. Absolute path to server.py. In the terminal a bare server.pyworks because you are already in that folder. Claude Desktop launches from its own working directory, so it needs the full path or you get "file not found."
  2. Absolute path to uv too. Claude Desktop starts with a minimal PATH and cannot find a bare "command": "uv". It fails with spawn uv ENOENT and no visible error. Run which uv and paste whatever it prints.

Config file location:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Step 5: Fully quit Claude, do not just close it

Cmd+Q. Closing the window leaves Claude running and it will not reload the config, so you will think you broke something. You did not. Quit it properly, then reopen.

Step 6: Prove it is real

Ask "what's the exact time on my machine?" with nothing connected first. It guesses, because it has to. Now connect the server and ask again. Claude calls the tool, a permission popup appears, you approve it, and the real time prints. Check your menu-bar clock, same minute. It is not guessing, it ran your code. Every tool call asks permission the first time, which is both the safety layer and proof the call actually happened.

Where this goes

The clock is the demo because it is undeniable, you can check it against your own screen. The value is the swap. Replace get_current_timewith a function that reads your Postgres, hits your company's API, or checks your calendar, and the ten lines around it do not change. Same decorator, same docstring, real function inside.

Honest limitations

STDIO only, local only. Claude Desktop connects to local servers over stdin and stdout. Letting Claude on the web reach your server needs an HTTP transport plus a proxy (mcp-remote), which is a separate build. That is the part 2.

If it does not connect, it is almost always the two absolute paths in Step 4, and neither gives you a useful error to work from.

The pip path is a landmine. Skip uv and the config command becomes python with the absolute path to server.py, and it only works if you already have Python 3.10+. Stock macOS 3.9 will not run it.

Permission fires on every tool the first time you call it. Good for safety, mildly repetitive in a demo.

Quick reference

  • Run it locally: uv run --with mcp server.py. Terminal looks frozen because a STDIO server waits silently; Ctrl+C stops it.
  • The mechanism: @mcp.tool() exposes the function, the docstring and type hints are its schema.
  • Never print() to stdout. Log to stderr with file=sys.stderr.
  • Config: Settings, Developer, Edit Config. Absolute path to both uv and server.py.
  • Restart: fully quit with Cmd+Q, not just close the window.

The AI Side Hustle Cookbook

Liked this guide? Shout me a coffee.

$4.99 gets you the full playbook: 50 recipes you can build, ship, and get paid for with Claude Code. Working code in every one. The pricing, the deploy, the pitfalls. Every revision free for life.

Shout me a coffee