Skip to content
Future Site
All news
Tutorial

Episode 2 of the series MCP Explained

Build your first MCP server in Python and TypeScript

6 min read

Thumbnail: Your first MCP server, MCP Explained episode 2

In episode 1 you learned what the Model Context Protocol is: a host (like Claude) runs a client for every server, and servers offer tools, resources and prompts. Now it is time to build one.

In this episode of MCP Explained you will write a small weather server with a single tool, in Python and in TypeScript side by side, run it over stdio, test it by hand, connect it to Claude, and give it a first resource. It takes about twenty lines in each language.

Build your first MCP server in Python and TypeScript. The player loads from youtube-nocookie.com when you press play.

What we will build

A server called weather with one tool, forecast(city), that returns tomorrow's forecast for a city. It returns a fixed string for now: the point is the MCP plumbing, not the weather data. Once the plumbing works, swapping the string for a real API call is a five-minute job.

The steps:

  1. Write the tool, in Python and in TypeScript.
  2. Run the server over stdio, so the client starts it as a local process.
  3. Test it in the MCP Inspector.
  4. Connect it to Claude Code and Claude Desktop.
  5. Add a resource.

Set up the project

In Python, the easiest way in is FastMCP, which handles the protocol for you. With uv:

uv init weather
cd weather
uv add fastmcp

In TypeScript, use the official SDK plus Zod, which describes the tool's inputs:

mkdir weather && cd weather
npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript @types/node

The server in Python

Here is the whole server:

from fastmcp import FastMCP

mcp = FastMCP("weather")

@mcp.tool
def forecast(city: str) -> str:
    """Tomorrow's weather forecast for a city."""
    return f"{city}: sunny, 21°C"

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

Three things happen here:

  • @mcp.tool registers a normal Python function as a tool.
  • The type hints (city: str) become the tool's input schema.
  • The docstring becomes the tool's description, the text the model reads to decide when to use it.

mcp.run() starts the server. By default it talks over stdio: JSON messages on standard input and output.

The same server in TypeScript

The weather server in TypeScript with registerTool and a Zod schema

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "weather", version: "1.0.0" });

server.registerTool("forecast", {
  description: "Tomorrow's weather forecast for a city",
  inputSchema: { city: z.string() },
}, async ({ city }) => ({
  content: [{ type: "text", text: `${city}: sunny, 21°C` }],
}));

await server.connect(new StdioServerTransport());

registerTool takes the name, a description and a Zod schema. The handler returns a list of content blocks, here a single block of text. The last line connects the server to the stdio transport. Compile it with npx tsc (set "type": "module" in package.json so top-level await works).

Same three ideas, whatever the language

Both versions say exactly the same three things about the tool:

Python (FastMCP) TypeScript (SDK)
Name the function name first argument of registerTool
Description the docstring description
Input schema type hints Zod schema

The client receives all three as JSON Schema when it asks for the list of tools. Get the description right and the model will know when your tool is useful; get the schema right and it will know exactly what to send.

What happens over stdio

Sequence: the client starts the server, initialize, tools/list, tools/call and the result

When Claude uses your server, the conversation looks like this:

  1. The client starts your server as a child process and sends initialize.
  2. The server answers with its capabilities.
  3. The client calls tools/list and receives forecast with its schema.
  4. When you ask about the weather, the model decides to use the tool and the client sends tools/call with {"city": "Rome"}.
  5. Your function runs and its result flows back to the model.

The one rule of stdio

Because the protocol travels on stdout, anything else you write there breaks it. A single print("fetching...") puts plain text in the middle of the JSON stream, and the client gives up, often without a clear error. Log to stderr instead:

import sys
print("fetching forecast...", file=sys.stderr)

In Node, use console.error, never console.log.

Test it with the MCP Inspector

Before you let a model near it, test the server on its own with the MCP Inspector:

Python:

uv run fastmcp dev server.py

TypeScript:

npx tsc
npx @modelcontextprotocol/inspector node build/server.js

It opens in your browser, lists every tool, resource and prompt, lets you call a tool with your own inputs and shows every JSON message. If something fails in the Inspector, the bug is in your server, not in the AI.

Connect it to Claude

Claude Code command and Claude Desktop config side by side

Claude Code needs one command: a name, then -- and the command that starts your server.

claude mcp add weather -- uv run /abs/path/server.py

Inside Claude Code, /mcp shows whether it is connected.

Claude Desktop reads a config file: open Settings → Developer → Edit Config and add:

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["run", "/abs/path/server.py"]
    }
  }
}

Use absolute paths (the app does not start in your project folder) and restart the app completely. Then just ask: "What's the weather in Rome tomorrow?" Claude asks for permission before running your tool, then answers with your data.

Add a resource

Tools are actions. Resources are context the app can read. A resource has a URI, only returns data, and the app or the user decides when to attach it to the conversation:

@mcp.resource("weather://cities")
def cities() -> str:
    """Cities we have forecasts for."""
    return "Rome, Milan, Naples, Turin"

In TypeScript the equivalent is server.registerResource(name, uri, metadata, handler).

Where to go next

  • Real data: call a weather API inside forecast instead of returning a fixed string.
  • Remote server: switch to the Streamable HTTP transport and deploy it for your whole team.
  • Security: validate inputs and limit what tools can touch (episode 4).
  • MCP Apps: return interactive UI, not just text (episode 3).

Practice

  1. Which part of a FastMCP tool becomes its description?
  2. Your server works in the Inspector but Claude Desktop shows it as failed. Name two likely causes.
  3. Why does print("debug") break a stdio server, and what should you use instead?
  4. Tool or resource? (a) "Send a message to Slack", (b) "The list of supported cities".
  5. In claude mcp add weather -- uv run server.py, what does everything after -- mean?

Answers: 1. The docstring. 2. A relative path in the config, or the app was not fully restarted (also: a stray print to stdout; check the logs in ~/Library/Logs/Claude on a Mac). 3. stdout carries the protocol, so plain text corrupts the JSON stream; log to stderr. 4. (a) a tool, (b) a resource. 5. It is the command Claude Code runs to start your server.

Next

In episode 3 we look at MCP Apps and WebMCP: what happens when tools return real interfaces instead of text. The whole series is on the MCP Explained page.