Your First MCP Server: One Tool, Two Stacks

A hands-on first MCP server in Python and TypeScript: install the SDK, define one tool, run it over stdio, and call it with the MCP Inspector.

Series: Building MCP Servers — Part 2 of 12

Part 1 was the map: a server exposes tools, resources, and prompts; a host reaches them through a client over a transport. This post is the territory. We’ll build the smallest server that does something real — one tool the model can call — run it over stdio, and prove it works by calling the tool ourselves. By the end you’ll have the loop every later post builds on: write a capability, run the server, poke it.

Every code sample has a Python tab and a TypeScript tab. The official SDKs mirror each other closely, so pick the stack you’ll actually use and ignore the other — the concepts are identical either way. This series targets the Python SDK mcp 1.28 (Python 3.10+) and the TypeScript SDK @modelcontextprotocol/sdk 1.29 with Zod 4 (Node 18+).

Set up the project

uv init calculator-server
cd calculator-server
uv add "mcp[cli]"
mkdir calculator-server && cd calculator-server
npm init -y && npm pkg set type=module
npm install @modelcontextprotocol/sdk zod
npm install -D tsx typescript

The [cli] extra on the Python side pulls in the mcp command-line helper; the zod dependency on the TypeScript side is how you’ll describe tool inputs, and the SDK leans on it directly. tsx just lets us run TypeScript without a separate compile step while developing.

The server

A tool is a function plus a description. You write the function; the SDK turns it into the JSON-RPC methods from Part 1 — including a JSON Schema for its arguments, derived straight from your type hints. Here’s a complete server exposing a single add tool:

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

mcp = FastMCP("calculator")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers and return the sum."""
    return a + b


if __name__ == "__main__":
    mcp.run()
// server.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: "calculator", version: "1.0.0" });

server.registerTool(
  "add",
  {
    title: "Add",
    description: "Add two numbers and return the sum.",
    inputSchema: { a: z.number(), b: z.number() },
  },
  async ({ a, b }) => ({
    content: [{ type: "text", text: String(a + b) }],
  })
);

await server.connect(new StdioServerTransport());

The shapes rhyme. In Python, FastMCP is the high-level server and the @mcp.tool() decorator registers the function; the docstring becomes the tool’s description and the a: int, b: int hints become its input schema. In TypeScript, registerTool takes the name, a config object (note the inputSchema is a Zod shape — a plain object of Zod types, not z.object({...})), and the handler. The handler returns content: a list of typed blocks, here one piece of text.

One asymmetry to file away: because Python infers the return type (-> int), FastMCP automatically advertises an output schema and returns a structured result alongside the text. The TypeScript handler above returns only text. Both are valid; we’ll make structured output explicit on both sides in the next post.

Run it

How you launch the server depends on the transport, and for a local tool that’s stdio — the host starts your server as a subprocess and talks to it over stdin/stdout. Python’s mcp.run() defaults to stdio; the TypeScript StdioServerTransport says the same thing out loud.

uv run server.py
npx tsx server.ts

Run that and… nothing visible happens. That’s correct. The server is sitting on stdin waiting for JSON-RPC messages — a process designed to be driven by something else, not chatted with directly. Press Ctrl-C and let’s drive it properly.

Poke it with the Inspector

The MCP Inspector is the standard way to exercise a server during development. It’s a one-off npx package that launches your server and talks to it for you — either in a browser UI or, handy for a quick check, straight from the command line. Point it at the same command you’d use to run the server:

# list the tools this server exposes
npx @modelcontextprotocol/inspector --cli uv run server.py --method tools/list

# call the add tool
npx @modelcontextprotocol/inspector --cli \
  uv run server.py --method tools/call \
  --tool-name add --tool-arg a=2 --tool-arg b=3
# list the tools this server exposes
npx @modelcontextprotocol/inspector --cli npx tsx server.ts --method tools/list

# call the add tool
npx @modelcontextprotocol/inspector --cli \
  npx tsx server.ts --method tools/call \
  --tool-name add --tool-arg a=2 --tool-arg b=3

The call returns the result block your handler produced:

{
  "content": [ { "type": "text", "text": "5" } ],
  "isError": false
}

Drop the --cli ... --method ... part — just npx @modelcontextprotocol/inspector uv run server.py — and you get the browser UI instead, with the same tools laid out as clickable forms. The CLI is faster for a sanity check; the UI is better for exploring. Either way you’ve now confirmed the round trip: the host lists your tool, calls it with arguments, and gets a result back.

Wiring it into a real host

The Inspector stands in for a host, but the point of a server is to be used by a real one. Hosts that speak MCP read a small config block that tells them how to launch your server — the same command, formalized. For a Claude Desktop–style host it looks like this (this is the documented config shape, not something the Inspector runs for you):

{
  "mcpServers": {
    "calculator": {
      "command": "uv",
      "args": ["--directory", "/abs/path/to/calculator-server", "run", "server.py"]
    }
  }
}

That’s the entire handoff. The host launches calculator, discovers the add tool, and from then on the model can call it whenever the conversation needs arithmetic it shouldn’t do in its head. Swap the command for npx tsx server.ts (with the right --directory/cwd) and the TypeScript server drops into exactly the same slot — which is the M+N promise from Part 1, made concrete.

Final thoughts

There’s almost nothing here, and that’s the feature. A tool is a typed function with a sentence of documentation; the SDK does the protocol; the transport is one line. Everything else in this series — structured results, resources, prompts, going remote, auth — hangs off this skeleton without changing its shape. Get this loop comfortable (edit the server, run it, poke it with the Inspector) and the rest is just filling in more interesting functions.

Next: Tools, Properly, where one add becomes real tools — rich input validation, structured output on both stacks, and errors the model can actually recover from.


Target keyword(s): mcp server tutorial, build mcp server python typescript.

Comments