Tutorial10 min readMay 5, 2026

How to Build Your First MCP Server

Building your own MCP is shorter than most people expect. The official SDK does the heavy lifting — protocol, schema, transport — so you can focus on what your tool actually does. This walkthrough takes a blank directory to a working server connected to Claude Desktop in about ten minutes.

Goal

Ship a working stdio MCP server with one tool

Time

~10 minutes

Prereq

Node.js 20+ or Python 3.11+, Claude Desktop installed

Steps

1

Pick a language and install the SDK

The official MCP SDK ships in TypeScript and Python — both are supported and well-maintained. Pick whichever language you reach for first when scripting. The example below uses TypeScript via npx; for Python, swap to `uv add mcp` and the same shape applies with idiomatic Python decorators.

# TypeScript / Node
mkdir my-first-mcp && cd my-first-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
npx tsc --init

# Python
uv init my-first-mcp && cd my-first-mcp
uv add mcp
2

Define one tool with a typed schema

Resist the urge to expose ten tools on day one. Start with a single, well-described tool that does one thing — agents work better with clear, narrow tools than with sprawling APIs. The example below exposes a single `echo` tool that returns whatever string the agent passes in.

// 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: "my-first-mcp",
  version: "0.1.0",
});

server.tool(
  "echo",
  "Returns whatever string you send it.",
  { message: z.string() },
  async ({ message }) => ({
    content: [{ type: "text", text: `Echo: ${message}` }],
  })
);

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

Test with the MCP Inspector before wiring to a client

The MCP Inspector is a browser-based UI that connects to your server over stdio, lists its tools, and lets you call them with arbitrary inputs. Run it before touching Claude Desktop — if the Inspector cannot see the tool, no client will either.

# In one terminal:
npx tsx src/index.ts

# In another:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
# Open the printed URL, click "List Tools", call "echo" with any input.
4

Wire the server into Claude Desktop

Once the Inspector confirms the tool works, register the server in claude_desktop_config.json. Use the absolute path to your built server so Claude can launch it from any working directory. Fully restart Claude — the new tool appears in the hammer icon.

// claude_desktop_config.json
{
  "mcpServers": {
    "my-first-mcp": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/my-first-mcp/src/index.ts"]
    }
  }
}
5

Smoke-test from a Claude conversation

Ask Claude: "Use the echo tool to say hello." Claude should call your tool with `{message: "hello"}` and return the response. If it does not call the tool, the manifest is wrong; if it calls the tool but fails, your handler is throwing — check the MCP logs in Settings → Developer.

6

Iterate: add more tools, polish the descriptions

Tool descriptions are how the model decides when to call you — write them like API docs, not like changelogs. Add a second tool only after the first feels reliable. When you are ready, package the server (npm publish or pip publish) so others can install it with one snippet.

Tool descriptions are the contract with the model

The string you pass as the tool description is how every agent in every client decides whether to call your tool. Treat it like a one-sentence API doc: lead with the verb, name the inputs, state what it returns. Vague descriptions ("does stuff with strings") are silently ignored by capable models.

Minimal Python equivalent

For reference — the same server in Python using the official SDK. Same protocol, same client config, idiomatic Python decorators.

server.py
from mcp.server.fastmcp import FastMCP

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

@mcp.tool()
def echo(message: str) -> str:
    """Echo a string back to the caller for testing."""
    return f"Echo: {message}"

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

When it does not work

Inspector shows the server but no tools

The server started but tool registration failed silently. Most often: a Zod schema typo, an async handler that throws on import, or `server.tool()` called after `server.connect()`. Add `console.error("registered echo")` before `server.connect()` to confirm the call ran.

Claude Desktop says "server failed to start"

Open Settings → Developer → MCP Logs. Common causes: the path in args is wrong, npx cannot resolve tsx, or a runtime error before the transport connects. Run the same command manually in a terminal — if it fails there, fix the runtime issue first.

Tool calls work but the model never picks them

Tool description is too vague or overlaps with built-in capabilities. Rewrite it as a one-sentence verb-led summary ("Echo a string back to the caller for testing") — agents weight descriptions heavily when deciding whether to call.

FAQ

Do I have to use stdio? Can I write a remote (HTTP) MCP server?

Yes to both. Stdio is the simplest local path and the only transport some clients support — start there. Once it works, the SDK exposes a Streamable HTTP transport too, which lets you deploy the server behind a public URL (Cloudflare Workers, Vercel, Fly all work) so ChatGPT and other remote-only clients can reach it.

Should I use TypeScript or Python?

Whichever feels natural. The TypeScript SDK is slightly ahead on examples and tooling; Python is cleaner if your handlers are calling Python-only libraries (NumPy, scikit-learn, in-house ML). The wire format is identical, and clients do not care which language served them.

Where should I put the server's logs?

Standard error (stderr). MCP reserves stdout for the protocol stream; anything you write there will corrupt the connection. `console.error()` in Node and `print(..., file=sys.stderr)` in Python both go to stderr by default.

How do I publish my MCP so others can install it?

Same as any other package. For Node: bundle to JS, add a "bin" entry in package.json, and `npm publish`. Once published, users can run it as `npx @your-org/your-mcp` from any client. For Python, `uv build` then `uv publish` — the `uvx` runner is the equivalent of npx for Python MCPs.

Next steps

One tool works. Now harden it before letting agents touch real systems — scope credentials, lock paths, audit the tool list.