Skip to main content
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 and have the CLI installed and authenticated.

1. Scaffold the project

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:
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);
});

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

3. Set the server name in ezforge.toml

[project]
name = "my-first-project"

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

4. Deploy

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

5. Test the tools

Test the health check:
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

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)
  • 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