# Build your first server

Build an MCP **server** — a program that exposes tools a model can call — and call its one tool, a US weather-alert lookup, from a client.

## Set up the project

You need Node.js 20 or later and nothing else. Create the project and install the SDK.

```sh
mkdir weather && cd weather
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src
```

`type=module` matters — the SDK ships ES modules only. `tsx` runs TypeScript directly, so there is no build step.

## Register a tool

Create `src/index.ts`: a `createServer` factory that builds an `McpServer` and registers one **tool** — a function the connected model can call.

```ts
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';

const NWS_API = 'https://api.weather.gov';

interface AlertsResponse {
    features: { properties: { event?: string; headline?: string } }[];
}

function createServer(): McpServer {
    const server = new McpServer({ name: 'weather', version: '1.0.0' });

    server.registerTool(
        'get-alerts',
        {
            description: 'Get the active weather alerts for a US state',
            inputSchema: z.object({
                state: z.string().length(2).describe('Two-letter US state code, e.g. CA')
            })
        },
        async ({ state }) => {
            const code = state.toUpperCase();
            const url = `${NWS_API}/alerts/active?area=${code}`;
            const res = await fetch(url, { headers: { 'User-Agent': 'mcp-weather-tutorial/1.0' } });
            if (!res.ok) {
                return { content: [{ type: 'text', text: `NWS API error: HTTP ${res.status}` }], isError: true };
            }
            const { features } = (await res.json()) as AlertsResponse;
            if (features.length === 0) {
                return { content: [{ type: 'text', text: `No active alerts for ${code}.` }] };
            }
            const lines = features.map(f => f.properties.headline ?? f.properties.event ?? 'Unnamed alert');
            return { content: [{ type: 'text', text: lines.join('\n') }] };
        }
    );

    return server;
}
```

`registerTool` takes a name, a config, and an async handler. `inputSchema` is a Zod schema — the only schema you write. From that one schema the SDK derives the JSON Schema the model sees, validates arguments before your handler runs, and infers the handler's argument types.

The handler returns **content**, a list of typed blocks — one `text` block here. `isError: true` marks a failed result the model can read and react to.

::: tip
Call `get-alerts` with `{ "state": "California" }` and the SDK rejects it before your handler runs. The result is the failure the model sees:

```text
Input validation error: Invalid arguments for tool get-alerts: state: Too big: expected string to have <=2 characters
```

:::

## Serve it over stdio

At the end of the file, hand the factory to `serveStdio`.

```ts
void serveStdio(createServer);
console.error('weather MCP server running on stdio');
```

`serveStdio` owns the **stdio transport**: it reads requests on stdin, writes responses to stdout, and calls `createServer` to build the instance that serves the connection.

::: warning
stdout is the protocol channel. Log with `console.error` — one `console.log` corrupts the JSON-RPC stream.
:::

## Run it

Start the server from the project root.

```sh
npx tsx src/index.ts
```

The banner lands on stderr, leaving stdout for the protocol:

```text
weather MCP server running on stdio
```

Nothing else happens: an stdio server waits on stdin for a client to start the conversation. Stop it with `Ctrl+C`.

## Call the tool

The **MCP Inspector** is a local web app for calling a server's tools directly — it launches the command you give it and connects over stdio.

```sh
npx @modelcontextprotocol/inspector npx tsx src/index.ts
```

In the browser tab it opens, click **Connect**, open the **Tools** tab, select `get-alerts`, enter a two-letter state code such as `TX`, and run it. The text block in the result lists each active alert headline for that state — the same content a model receives when it calls your tool.

## Pick a transport

Your server speaks stdio because a host launches it as a local process and owns its lifetime. To host one endpoint that many clients connect to, serve the same `createServer` factory over [HTTP](https://ts.sdk.modelcontextprotocol.io/v2/serving/http.md) instead.

Next on this path, [Plug into a real host](https://ts.sdk.modelcontextprotocol.io/v2/get-started/real-host.md) registers this server in VS Code, Claude Code, and Cursor; [Tools](https://ts.sdk.modelcontextprotocol.io/v2/servers/tools.md) goes deeper on what a tool can return.

## Recap

- `registerTool(name, config, handler)` registers a tool; `inputSchema` is the one Zod schema you write.
- The SDK validates every call against that schema and rejects bad arguments before your handler runs.
- `serveStdio(createServer)` builds the server from your factory and serves it on stdin/stdout.
- stdout carries the protocol; log to stderr.
- `npx @modelcontextprotocol/inspector <command>` exercises any stdio server without a host.
