Skip to the page
Tecyfy

How to build an MCP server in Python, and fix it when it does not show up

An MCP server in Python is one file: an MCPServer, a few functions marked as tools, and a run line. Build a working weather server in about 40 lines, connect it to Claude for Desktop and Claude Code, and fix the 5 mistakes that keep a server out of the menu.

Tecyfy · Published · 8 min read

A laptop chat window plugged by a long cable into a small server box with sun and rain symbols: a weather MCP server connected to an AI app

To build an MCP server in Python, install the official SDK with uv add "mcp[cli]", create an MCPServer, mark each function the model may call with @mcp.tool(), and end the file with mcp.run(transport="stdio"). The SDK reads each function's type hints and docstring and turns them into the tool's definition, so a working server is one file of about 40 lines. Then tell your client how to start it, with absolute paths, and never print() anything.

That last rule causes more broken servers than any bug in the tools themselves. This guide builds the weather server from the official tutorial, connects it to Claude for Desktop and Claude Code, and then fixes the 5 mistakes that keep it from showing up.

What an MCP server is

The Model Context ProtocolModel Context ProtocolAn open standard for connecting AI applications to tools and data. A system offered once as an MCP server works in any application that speaks MCP. is a standard way for an AI app to use outside tools. The app (Claude for Desktop, Claude Code, an IDE) is the client. Your server says which tools it has, with a name, a description and the inputs each takes; the model decides when to call one; the client runs the call through your server and hands the result back. It is tool useTool UseGiving a model a list of things it can ask to have done, such as search the web or read a file. The model asks; the program around it does the work and hands back the result. with the plumbing standardised, so one server works in every client that speaks MCP.

A local server talks over stdio: the client starts your program and they exchange JSON-RPC messages through its standard input and output. A remote server talks over HTTP and many people can share it. Start with stdio. It needs no hosting and no sign-in.

Set up the project

You need Python 3.10 or newer and the MCP Python SDK 2.0 or newer. With uv:

uv init weather
cd weather
uv venv
source .venv/bin/activate
uv add "mcp[cli]"
touch weather.py

Write the server

The server below has 2 tools that call the US National Weather Service, which needs no key. It is the official tutorial's code, trimmed to the parts that matter:

import logging
from typing import Any

import httpx2
from mcp.server import MCPServer

logger = logging.getLogger(__name__)
mcp = MCPServer("weather")

NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"


async def make_nws_request(url: str) -> dict[str, Any] | None:
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    async with httpx2.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            logger.exception("NWS request failed: %s", url)
            return None


@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g. CA, NY)
    """
    data = await make_nws_request(f"{NWS_API_BASE}/alerts/active/area/{state}")
    if not data or "features" not in data:
        return "Unable to fetch alerts or no alerts found."
    if not data["features"]:
        return "No active alerts for this state."
    return "\n---\n".join(
        f"{f['properties'].get('event')}: {f['properties'].get('areaDesc')}"
        for f in data["features"]
    )


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get the weather forecast for a location in the US.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """
    points = await make_nws_request(f"{NWS_API_BASE}/points/{latitude},{longitude}")
    if not points:
        return "Unable to fetch forecast data for this location."
    forecast = await make_nws_request(points["properties"]["forecast"])
    if not forecast:
        return "Unable to fetch detailed forecast."
    return "\n---\n".join(
        f"{p['name']}: {p['temperature']}°{p['temperatureUnit']}, {p['detailedForecast']}"
        for p in forecast["properties"]["periods"][:5]
    )


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

3 things in it do the work:

  • @mcp.tool() makes the function a tool. The name, the type hints and the docstring become what the model reads, so write the docstring for the model: what the tool does, and what each input means, with an example. A vague description is the most common reason a model calls the wrong tool or none.
  • httpx2 is the HTTP client the SDK already depends on, so there is nothing more to install.
  • logger, not print(). Over stdio, standard output belongs to the protocol. A print() writes into the middle of a JSON-RPC message and breaks it. The logging module writes to stderr, which the client keeps in a log file for you.

Return text the model can use directly. A short readable answer, or a clear sentence when something fails, beats a raw JSON dump: the whole result goes into the model's context and costs tokens.

Check it before connecting a client

Run uv run weather.py. It should sit and wait with no output, because it is listening on stdin. Press Ctrl+C. To see the tools as a client will, list them with the MCP Inspector:

npx @modelcontextprotocol/inspector --cli uv run weather.py --method tools/list

Both get_alerts and get_forecast should appear with their descriptions. If they do, every remaining problem is in the client's config.

Connect it to Claude for Desktop

Open the config file, creating it if it does not exist:

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

Add the server under mcpServers, with the absolute path to the project folder:

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["--directory", "/Users/you/projects/weather", "run", "weather.py"]
    }
  }
}

Quit Claude completely and open it again. The weather tools appear in the Connectors menu. Ask "What are the active weather alerts in Texas?" and Claude calls get_alerts to answer.

Connect it to Claude Code

One command, with the server's start command after --:

claude mcp add weather -- uv --directory /Users/you/projects/weather run weather.py

Run /mcp in a session to check it is connected. To share the server with your team, add it with project scope so it goes into the repository's .mcp.json. Our Claude Code MCP guide covers the scopes, environment variables and signing in to remote servers.

Why your MCP server does not show up

When the Inspector lists your tools but the client does not, the cause is almost always one of 5 things. Pick the ones that might apply to you:

Try itWhy your MCP server does not show up
Mistakes

Claude for Desktop:Not in the Connectors menu

claude_desktop_config.json

{  "mcpServers": {    "weather": {      "command": "/Users/you/.local/bin/uv",      "args": [        "--directory",        "./weather",        "run",        "weather.py"      ]    }  }}

weather.py

@mcp.tool()async def get_alerts(state: str) -> str:    """Get weather alerts for a US state."""    logger.info("Fetching alerts for %s", state)    data = await make_nws_request(url)
  1. 1. Relative path

    Claude starts the server from its own folder, so a relative path points nowhere and the server never starts.

    Fix: Use the absolute path to the project folder (run pwd inside it). On Windows, double the backslashes.

The server does not appear. Fix the marked line, then quit Claude fully and open it again.

Follows the official MCP server tutorial (Python) and its troubleshooting section. Claude for Desktop writes mcp.log and one mcp-server-NAME.log per server, with the server's stderr, in ~/Library/Logs/Claude on macOS.

The logs tell you which. On macOS run:

tail -n 20 -f ~/Library/Logs/Claude/mcp*.log

mcp.log records each connection and why it failed. mcp-server-weather.log holds everything your server wrote to stderr, which is where your logger lines and any Python traceback land. On Linux the folder is ~/.config/Claude/logs/.

If the server shows up but a tool call fails, run the same call through the Inspector. If it fails there too, the bug is in your code; if it works there, look for anything writing to stdout, including a library that prints on import.

Before you give it to anyone

A tool is a door into whatever it touches. A model will call it with whatever inputs the conversation steers it towards, including text from a web page or file that carries injected instructionsPrompt InjectionAn attack that hides instructions in text a model reads, such as a web page, an email or a file, so the model follows the attacker instead of you.. Keep each tool narrow: read-only where it can be, one clear job each, inputs checked in code, and no secrets in what it returns. Give it its own credentials with only the access it needs, as what a non-human identity is explains. The same questions apply the other way round: before you install someone else's server, vet it in 10 minutes.

Not every integration needs to be an MCP server. If a model only needs one API in one app, a plain function call is less to run. MCP vs function calling compares the two, and MCP vs API, CLI or skills shows what many connected servers cost in tokens before the model does anything.

Questions

How do I build an MCP server in Python?
Install the official SDK with uv add "mcp[cli]", create an MCPServer, mark each function the model may call with @mcp.tool(), and end the file with mcp.run(transport="stdio"). The SDK turns each function's type hints and docstring into the tool's definition. Then add the server to your client's config with the command that starts it, using absolute paths.
Why is my MCP server not showing up in Claude for Desktop?
The usual causes are a relative path in claude_desktop_config.json, a command such as uv that Claude cannot find without its full path, invalid JSON such as a trailing comma, and closing the window instead of quitting the app, which keeps the old config. Fix the file, quit Claude fully, open it again, and read mcp.log and mcp-server-NAME.log in ~/Library/Logs/Claude on macOS.
Why can't I use print() in an MCP server?
A stdio MCP server talks to the client in JSON-RPC messages over standard output. Anything else written to stdout, such as a print(), lands in the middle of that stream and corrupts it, so tool calls fail or the server disconnects. Use the logging module, which writes to stderr; the client keeps stderr in a log file. An HTTP server can log to stdout.
What is the difference between an MCP server over stdio and over HTTP?
A stdio server runs on your machine as a program the client starts, and they talk over its standard input and output. It suits personal tools and local files. An HTTP server runs somewhere else and many clients connect to it over the network, which suits shared tools, but it needs authentication and its own hosting.
How do I add my MCP server to Claude Code?
Run claude mcp add, a name, then two dashes and the command that starts the server, for example claude mcp add weather -- uv --directory /absolute/path/weather run weather.py. Then run /mcp inside a Claude Code session to check that it is connected.