> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ezforge.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Your First Server

> Build and deploy a custom MCP server from scratch

This guide walks you through building a custom MCP server with real tools, deploying it to ezForge, and connecting it to an AI assistant.

## What you'll build

A simple MCP server with two tools:

* **`get_weather`** — fetches current weather for a city (simulated)
* **`list_cities`** — returns a list of supported cities

## Prerequisites

Completed the [Quickstart](/getting-started/quickstart) and have the CLI installed and authenticated.

## 1. Scaffold the project

```bash theme={null}
git clone https://github.com/ezforgeai/template-nodejs-mcp-server weather-server
cd weather-server
npm install
```

## 2. Add your tools

Replace the contents of `src/index.ts` with:

```typescript theme={null}
import express from 'express';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  McpError,
} from '@modelcontextprotocol/sdk/types.js';

const PORT = parseInt(process.env['PORT'] ?? '8080', 10);

// Simulated weather data
const WEATHER: Record<string, { temp: number; condition: string }> = {
  chicago: { temp: 22, condition: 'partly cloudy' },
  amsterdam: { temp: 14, condition: 'rainy' },
  sydney: { temp: 28, condition: 'sunny' },
};

function createMcpServer(): Server {
  const server = new Server(
    { name: 'weather-server', version: '1.0.0' },
    { capabilities: { tools: {} } },
  );

  server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: [
      {
        name: 'get_weather',
        description: 'Get current weather for a city.',
        inputSchema: {
          type: 'object',
          properties: {
            city: { type: 'string', description: 'City name (chicago, amsterdam, sydney).' },
          },
          required: ['city'],
        },
      },
      {
        name: 'list_cities',
        description: 'List cities with available weather data.',
        inputSchema: { type: 'object', properties: {} },
      },
    ],
  }));

  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    if (name === 'list_cities') {
      return { content: [{ type: 'text', text: Object.keys(WEATHER).join(', ') }] };
    }

    if (name === 'get_weather') {
      const { city } = args as { city: string };
      const w = WEATHER[city.toLowerCase()];
      if (!w) throw new McpError(ErrorCode.InvalidParams, `Unknown city: ${city}`);
      return { content: [{ type: 'text', text: `${city}: ${w.temp}°C, ${w.condition}` }] };
    }

    throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
  });

  return server;
}

const app = express();
const transports = new Map<string, SSEServerTransport>();

app.get('/healthz', (_req, res) => { res.status(200).json({ status: 'ok' }); });

app.get('/sse', async (_req, res) => {
  const transport = new SSEServerTransport('/message', res);
  transports.set(transport.sessionId, transport);
  transport.onclose = () => { transports.delete(transport.sessionId); };
  await createMcpServer().connect(transport);
});

app.post('/message', express.json(), async (req, res) => {
  const sessionId = req.query['sessionId'] as string;
  const transport = transports.get(sessionId);
  if (!transport) { res.status(404).json({ error: 'Session not found' }); return; }
  await transport.handlePostMessage(req, res, req.body);
});

app.listen(PORT, () => console.log(`Weather server listening on port ${PORT}`));
```

## 3. Set the server name in ezforge.toml

```toml theme={null}
[project]
name = "my-first-project"

[server]
name = "weather-server"
region = "ord"
cpu = "shared-1x"
memory = "256mb"
```

## 4. Deploy

```bash theme={null}
ezforge deploy
```

Your server is now live at `https://weather-server.mcp.ezforge.ai`.

## 5. Test the tools

Test the health check:

```bash theme={null}
curl https://weather-server.mcp.ezforge.ai/healthz
```

Connect from any MCP client using the SSE endpoint:

```
https://weather-server.mcp.ezforge.ai/sse
```

## 6. View logs

```bash theme={null}
ezforge logs weather-server
```

## What's next?

* **Add secrets** — Store API keys as environment variables with `ezforge env set`
* **Enable auth** — Protect your server with OAuth 2.1 (see [Authentication](/getting-started/authentication))
* **Set up auto-stop** — Configure idle timeout in `ezforge.toml` to save costs
* **Deploy updates** — Push changes with `ezforge deploy`; auto-rollback protects you from bad deploys
