The best way to understand MCP is to build a server. This guide takes you from an empty folder to a working server with a tool and a resource, connected to a real client. We use the official TypeScript SDK; the Python equivalent is at the end. If you want the mental model first, read MCP fundamentals before you start.
How do I scaffold an MCP server project?
Install the SDK and Zod (the SDK uses it to validate tool inputs):
npm install @modelcontextprotocol/sdk zodOne thing to get right up front: import from the SDK's subpaths, with the .js
extension, even in TypeScript. These are ESM subpath exports.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as z from "zod";If you copy an import that reads @modelcontextprotocol/server (no /sdk) from
some docs, it will not resolve against the published package. Use the /sdk/...
paths above.
How do I expose a tool in an MCP server?
A tool needs a name, a config (title, description, and an input schema), and an async handler that returns content. Here is a complete, minimal server with one tool:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as z from "zod";
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.registerTool(
"calculate-bmi",
{
title: "BMI Calculator",
description: "Calculate Body Mass Index from weight and height.",
inputSchema: { weightKg: z.number(), heightM: z.number() },
},
async ({ weightKg, heightM }) => ({
content: [
{
type: "text",
text: String(weightKg / (heightM * heightM)),
},
],
}),
);
const transport = new StdioServerTransport();
await server.connect(transport);The schema is a plain object of Zod validators.Common gotchaPass the raw shape ({ weightKg: z.number() }), not z.object({ ... }).Wrapping it in z.object is the usual slip, and the SDK then fails to derive the field list, so the model sees a tool with no parameters. The SDK turns it into the JSON
schema the model sees, and validates incoming calls against it before your
handler runs. The handler returns a content array; text is the common case,
but you can also return images or embedded resources.
What is an MCP resource and how do I add one?
A resource is read-only context. Register one with a name, a URI, some metadata, and a reader:
server.registerResource(
"config",
"config://app",
{
title: "Application Config",
description: "Current application configuration.",
mimeType: "text/plain",
},
async (uri) => ({
contents: [{ uri: uri.href, text: "log_level=info\nregion=eu" }],
}),
);How do I run my MCP server and connect it to a client?
During development, run the server straight from TypeScript:Common gotchaA stdio server speaks JSON-RPC over stdout, so a stray console.log inside a handler writes garbage into that same stream and the client drops the connection with a cryptic parse error.Send all your logging to stderr instead (console.error), which the protocol leaves alone.
npx tsx server.tsThen connect it to Claude Code so the model can use it:
claude mcp add --transport stdio my-server -- npx tsx /absolute/path/to/server.tsTo inspect the server on its own, without a model in the loop, use the Inspector. It gives you a UI to list and call your tools and resources directly, which is the fastest way to confirm everything works before wiring it into an agent:
npx @modelcontextprotocol/inspectorThe Python version
Python's SDK has a high-level API called FastMCP that mirrors this closely:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
def calculate_bmi(weight_kg: float, height_m: float) -> float:
"""Calculate Body Mass Index."""
return weight_kg / (height_m ** 2)
if __name__ == "__main__":
mcp.run()Install it with pip install "mcp[cli]". Decorate functions with @mcp.tool(),
@mcp.resource(...), and @mcp.prompt(), and run.
How do I deploy an MCP server to production?
Once it works locally, the path to production is the transport: swap the stdio transport for Streamable HTTP, host it, and add auth. That move from local to networked is covered in local vs remote MCP servers. Keep secrets in environment variables, scope what the server can touch (see MCP security basics), and you have a capability any MCP client can connect to. To see servers other people have already built, browse useful MCP servers.