Skip to content

Setup and Your First Server

ToolVersionWhy
Node.js22.19 or newerRuns the TypeScript SDK and the MCP Inspector (the Inspector requires ≥ 22.19)
Python3.10 or newerRuns the Python SDK
uvany recentFast Python project and dependency manager; uvx runs published servers without installing them

Check with node --version, python3 --version, and uv --version. Everything else is fetched by the package managers.

Terminal window
mkdir hello-mcp && cd hello-mcp
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D tsx typescript

@modelcontextprotocol/server is the server half of the TypeScript SDK (the client half is a separate package). The Python package mcp contains both halves.

The server below exposes a single tool, get-forecast, and speaks stdio — the transport a host uses to run a server as a subprocess.

server.ts
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
serveStdio(() => {
const server = new McpServer({ name: 'weather', version: '1.0.0' });
server.registerTool(
'get-forecast',
{
description: 'Get the weather forecast for a city',
inputSchema: z.object({ city: z.string() }),
},
async ({ city }) => ({
content: [{ type: 'text', text: `Sunny in ${city} all week.` }],
}),
);
return server;
});

What each piece does:

  • McpServer / MCPServer — the server object. name and version become io.modelcontextprotocol/serverInfo in results and the server/discover response.
  • registerTool / @mcp.tool() — registers a tool. TypeScript takes an explicit Zod inputSchema (imported from zod/v4) and hands you typed arguments. Python derives the JSON Schema from the function’s type hints and the description from its docstring.
  • The handler’s return — TypeScript returns a result object with a content array. Python lets you return a plain string, which the SDK wraps as a text content block.
  • serveStdio / mcp.run() — wires the server to stdin/stdout. serveStdio takes a factory so the SDK can build a fresh server per connection; the same factory shape is used later for HTTP.

On the wire, this server answers tools/list with:

{
"resultType": "complete",
"tools": [
{
"name": "get-forecast",
"description": "Get the weather forecast for a city",
"inputSchema": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
]
}

That is the whole contract a host needs.

The MCP Inspector is the reference developer tool: one npx package with a web UI, a CLI, and a terminal UI, all sharing the same connection code. Point it at your server’s launch command:

Terminal window
npx @modelcontextprotocol/inspector npx tsx server.ts

The command prints a URL with a one-time session token. Open it, and you get a Tools tab listing get-forecast, a form generated from its inputSchema, and a monitoring sidebar that shows every JSON-RPC message as it crosses — the server/discover, the tools/list, and your tools/call. Reading that pane against the previous lesson is the fastest way to make the wire format real.

sequenceDiagram
  participant You as Browser
  participant I as Inspector
  participant S as server.ts (subprocess)
  You->>I: open session URL
  I->>S: spawn, write JSON-RPC to stdin
  I->>S: server/discover
  S-->>I: capabilities (tools)
  I->>S: tools/list
  S-->>I: get-forecast
  You->>I: fill city, click Run
  I->>S: tools/call get-forecast
  S-->>I: content: Sunny in Lisbon all week.
What the Inspector does when you connect

For CI, shell pipelines, or a quick sanity check, the same package has a CLI mode that runs one method and exits:

Terminal window
npx @modelcontextprotocol/inspector --cli npx tsx server.ts --method tools/list
npx @modelcontextprotocol/inspector --cli npx tsx server.ts \
--method tools/call --tool-name get-forecast --tool-arg city=Lisbon

Add --format json and pipe to jq when you want to assert on the output.

Over stdio, stdout is the protocol channel. The client reads it one line at a time and parses each line as JSON-RPC. The server must not write anything else there. One stray console.log("starting…") or print("debug") and the client sees a line that is not JSON, and the connection breaks in a way that looks like the server hung.

Log to stderr instead. The transport spec explicitly allows the server to write UTF-8 to stderr for informational, debug, and error output, and tells clients not to treat stderr as an error signal.

console.error('starting weather server'); // fine: stderr
// console.log(...) // never in a stdio server

The same rule applies to any library your server imports: if a dependency prints a banner to stdout on import, it will break your server before the first message.

A working MCP server, a way to exercise it, and the wire traffic to prove what it is doing. From here the course builds outward: the Server Primitives module fills the server with tools, resources, and prompts; the Transports module moves it from a subprocess to an HTTPS endpoint.

In a stdio server, why is a stray print to stdout a fatal bug?
Where should a stdio server write its logs?
Which command runs a one-off tools/list against a TypeScript server and exits?