Getting Started2026-07-1912 min read

Model Context Protocol Beginner's Guide: Zero to First Server

A complete, hands-on walkthrough: build a real two-tool MCP server in Python and TypeScript, test it with the MCP Inspector, and connect it to Claude Desktop.

Beginner Q&A forumCode sharing section

Model Context Protocol (MCP) is the open standard, originally released by Anthropic in November 2024, that lets an AI application discover and call external tools, read external resources, and reuse prompt templates through one consistent client-server interface. Instead of writing custom glue code for every model and every tool combination, you write one MCP server, and any MCP-compatible client — Claude Desktop, an IDE like Cursor or VS Code, or your own agent runtime — can use it immediately.

This guide builds a working MCP server from an empty folder to a tool call you can see execute inside Claude Desktop, in both Python and TypeScript. You'll type every command, read every line of code, and understand what each one does — no copy-pasting a finished repo and hoping it works. By the end you'll have a server exposing two real tools, and you'll know how to add more.

What You'll Build

A small "unit converter" MCP server with two tools: convert_temperature, which converts between Celsius and Fahrenheit, and convert_distance, which converts between kilometers and miles. It's deliberately simple — the goal is to see the full request/response cycle clearly, not to get lost in a real integration's error handling. Once this pattern clicks, wiring up a real API (Slack, GitHub, a database) is the same shape with more fields.

Prerequisites

  • Node.js 18 or later (for the TypeScript path) — check with node --version.
  • Python 3.10 or later (for the Python path) — check with python3 --version.
  • Claude Desktop installed, or another MCP-compatible client.
  • A terminal and a text editor. No prior MCP experience needed — this assumes only that you can write basic Python or TypeScript.

You don't need both languages — pick whichever you're more comfortable with. The two paths below build the identical server; skip to whichever section applies.

The Three Building Blocks, Briefly

Before writing code, it helps to know what an MCP server actually exposes. There are three primitives, and this guide's example only needs the first one:

  • Tools — functions the model can call, each with a name, a description written for the model (not for a human reading docs), and a JSON Schema describing its inputs. This is what we're building today.
  • Resources — read-only data addressed by URI that a client can pull into context on demand, instead of the server pushing everything up front.
  • Prompts — reusable, parameterized message templates a client can surface to the user as a starting point for a conversation.

Communication between client and server runs over JSON-RPC 2.0. For a local server like the one you're about to build, that happens over stdio — the client launches your server as a child process and talks to it over its standard input and output streams. There's no networking, no ports, no auth to configure for this first server.

Part 1: Building the Server in Python

The official Python SDK ships a high-level FastMCP class that turns a plain function into a tool using a decorator — you don't hand-write JSON Schema or register request handlers yourself.

Step 1: Set Up the Project

mkdir unit-converter-mcp
cd unit-converter-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]"

The [cli] extra pulls in the MCP Inspector dependency you'll use later to test the server without Claude Desktop in the loop.

Step 2: Write the Server

Create a file named server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("unit-converter")

@mcp.tool()
def convert_temperature(value: float, from_unit: str, to_unit: str) -> str:
    """Convert a temperature between Celsius and Fahrenheit.

    Args:
        value: The numeric temperature to convert.
        from_unit: Either "celsius" or "fahrenheit".
        to_unit: Either "celsius" or "fahrenheit".
    """
    if from_unit == to_unit:
        return f"{value} {from_unit}"

    if from_unit == "celsius" and to_unit == "fahrenheit":
        result = (value * 9 / 5) + 32
    elif from_unit == "fahrenheit" and to_unit == "celsius":
        result = (value - 32) * 5 / 9
    else:
        raise ValueError("from_unit and to_unit must be 'celsius' or 'fahrenheit'")

    return f"{round(result, 2)} {to_unit}"


@mcp.tool()
def convert_distance(value: float, from_unit: str, to_unit: str) -> str:
    """Convert a distance between kilometers and miles.

    Args:
        value: The numeric distance to convert.
        from_unit: Either "km" or "miles".
        to_unit: Either "km" or "miles".
    """
    if from_unit == to_unit:
        return f"{value} {from_unit}"

    if from_unit == "km" and to_unit == "miles":
        result = value * 0.621371
    elif from_unit == "miles" and to_unit == "km":
        result = value / 0.621371
    else:
        raise ValueError("from_unit and to_unit must be 'km' or 'miles'")

    return f"{round(result, 2)} {to_unit}"


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

Notice what you didn't have to write: no JSON Schema, no request handler registration, no transport setup. FastMCP reads the function's type hints and docstring and generates the tool schema automatically. The docstring matters more than it looks — it's what the model reads to decide when and how to call the tool, so write it the way you'd explain the function to someone who can't see your code, not the way you'd write an internal comment.

Step 3: Run It

python3 server.py

If nothing prints and the terminal just hangs, that's correct — the server is sitting on stdio waiting for a client to connect over stdin/stdout. Press Ctrl+C to stop it; you'll launch it properly through the Inspector or Claude Desktop next.

Part 2: Building the Same Server in TypeScript

If you'd rather use TypeScript, this section builds the identical two-tool server using the official @modelcontextprotocol/sdk package and zod for schema validation.

Step 1: Set Up the Project

mkdir unit-converter-mcp
cd unit-converter-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init --module nodenext --target es2022 --outDir build --rootDir src

Step 2: Write the Server

Create src/index.ts:

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: "unit-converter",
  version: "1.0.0",
});

server.tool(
  "convert_temperature",
  "Convert a temperature between Celsius and Fahrenheit",
  {
    value: z.number().describe("The numeric temperature to convert"),
    fromUnit: z.enum(["celsius", "fahrenheit"]),
    toUnit: z.enum(["celsius", "fahrenheit"]),
  },
  async ({ value, fromUnit, toUnit }) => {
    if (fromUnit === toUnit) {
      return { content: [{ type: "text", text: `${value} ${fromUnit}` }] };
    }

    const result =
      fromUnit === "celsius"
        ? (value * 9) / 5 + 32
        : ((value - 32) * 5) / 9;

    return {
      content: [{ type: "text", text: `${Math.round(result * 100) / 100} ${toUnit}` }],
    };
  }
);

server.tool(
  "convert_distance",
  "Convert a distance between kilometers and miles",
  {
    value: z.number().describe("The numeric distance to convert"),
    fromUnit: z.enum(["km", "miles"]),
    toUnit: z.enum(["km", "miles"]),
  },
  async ({ value, fromUnit, toUnit }) => {
    if (fromUnit === toUnit) {
      return { content: [{ type: "text", text: `${value} ${fromUnit}` }] };
    }

    const result = fromUnit === "km" ? value * 0.621371 : value / 0.621371;

    return {
      content: [{ type: "text", text: `${Math.round(result * 100) / 100} ${toUnit}` }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

The third argument to server.tool() is a Zod shape, not raw JSON Schema — the SDK converts it for you and validates every incoming call against it before your handler ever runs, so malformed arguments never reach your function body.

Step 3: Build and Run It

npx tsc
node build/index.js

Same as the Python version — a hanging terminal with no output means it's working correctly and waiting on stdio.

Testing With the MCP Inspector

Before wiring anything into Claude Desktop, use the official Inspector to call your tools directly and see the raw JSON-RPC traffic. It's the fastest way to catch a schema mistake.

# Python version
npx @modelcontextprotocol/inspector python3 server.py

# TypeScript version
npx @modelcontextprotocol/inspector node build/index.js

This opens a local web UI in your browser. Click "List Tools" to confirm both convert_temperature and convert_distance show up with the schema you expect, then use the "Call Tool" form to run one with real arguments and see the response come back. If a tool is missing or its schema looks wrong, fix it here before touching Claude Desktop's config — it's a much faster feedback loop.

Connecting to Claude Desktop

Once the Inspector confirms your server works, add it to Claude Desktop's config file so Claude can launch and use it automatically.

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

If the file doesn't exist yet, create it. For the Python version:

{
  "mcpServers": {
    "unit-converter": {
      "command": "/absolute/path/to/.venv/bin/python3",
      "args": ["/absolute/path/to/unit-converter-mcp/server.py"]
    }
  }
}

For the TypeScript version, point at the compiled output instead:

{
  "mcpServers": {
    "unit-converter": {
      "command": "node",
      "args": ["/absolute/path/to/unit-converter-mcp/build/index.js"]
    }
  }
}

Use absolute paths, not relative ones — Claude Desktop launches your server from its own working directory, not the one you had open in your terminal. Save the file and fully quit and reopen Claude Desktop (closing the window isn't enough on macOS; use Cmd+Q). You should see a small tools icon in the chat input once it picks up the new server — click it to confirm unit-converter is listed. Now ask Claude something like "Convert 100 kilometers to miles" and watch it call the tool.

Troubleshooting

  • Server doesn't show up in Claude Desktop: check the path in your config is absolute and correct, and that you fully restarted the app. Claude Desktop logs MCP server output to ~/Library/Logs/Claude/mcp*.log on macOS — check there first.
  • "Command not found" in the logs: the command field needs a full path to the interpreter (e.g. your venv's python3, not just python3), since Claude Desktop doesn't inherit your shell's PATH the way a terminal does.
  • Tool call fails with a schema error: almost always a mismatch between the type hint/Zod type and what the model actually sent. Re-check with the Inspector, which shows you the exact JSON-RPC payload.
  • Server starts then immediately exits: usually an uncaught exception during startup. Run it directly in a terminal first (not through Claude Desktop) so you can see the Python traceback or Node stack trace.

Next Steps

From here, the natural next additions are a resource (expose read-only data by URI instead of requiring a tool call to fetch it) and moving off stdio to Streamable HTTP once you need the server to run remotely and serve more than one client at a time. Browse the MCP server directory to see production servers built on the same primitives you just used, and read the MCP vs API comparison if you're deciding whether an existing REST API you own should get an MCP server in front of it.

Frequently Asked Questions

Do I need both Python and TypeScript?

No. Pick whichever language you're already comfortable with — both SDKs are officially maintained and produce functionally identical servers for this use case. Teams sometimes standardize on one language across all their internal MCP servers purely for consistency, not because one SDK is more capable than the other.

Why does the server hang with no output when I run it directly?

That's expected. An stdio-based MCP server communicates over its standard input and output streams, waiting for a client to send it JSON-RPC messages. With no client connected, it just sits there listening — that's correct behavior, not a crash.

Can I test my server without installing Claude Desktop?

Yes — the MCP Inspector (npx @modelcontextprotocol/inspector <command>) runs your server and gives you a browser UI to list and call tools directly, with no client app required. It's the recommended way to develop and debug before connecting to any real client.

Do I need to write JSON Schema by hand?

Not with either SDK shown here. Python's FastMCP generates the schema from your function's type hints and docstring; the TypeScript SDK generates it from the Zod shape you pass to server.tool(). You only write raw JSON Schema if you drop down to the SDKs' lower-level APIs, which most servers never need.

What's the difference between a tool and a resource?

A tool is a function the model actively calls, usually to take an action or compute something. A resource is passive, read-only data addressed by a URI that a client can pull into context when it decides it's relevant — closer to a file the model can read than a function it invokes. This guide's example only uses tools because "convert this value" is inherently an action, not a piece of data to read.

My tool works in the Inspector but Claude Desktop never calls it — why?

This is almost always the tool's description, not a bug. Claude decides whether to call a tool based on its name and description compared against what you asked — if the description is vague or doesn't match how you're phrasing your request, it may not get selected. Try rephrasing your prompt to more directly match the tool's stated purpose, and make the description specific about what the tool does and when to use it.

Join the Discussion

Community Q&A (0 questions)

No questions yet. Ask the first one!

Code Snippets (0)

No code snippets shared yet. Be the first to contribute!