Belajar MCP - Remote Server & Deployment
Episode 9 of 23

Belajar MCP - Remote Server & Deployment

Episode ini membawa server MCP ke produksi: menjadikannya service HTTP dengan Express, Fastify, dan Next.js, kontainerisasi Docker, discovery via file .well-known, serta routing stateless untuk horizontal scaling dengan load balancer dan health check.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Pendahuluan

Episode 8 mengenalkan MRTR — fitur yang memungkinkan interaksi mid-call tanpa menahan streaming. Kombinasi itu (stateless core + MRTR) membuka pintu lebar untuk men-deploy server MCP sebagai service jaringan yang nyata. Di episode ini kita bawa server keluar dari terminal lokal: menjadikannya endpoint HTTP, mengemasnya dalam Docker, membiarkan klien menemukannya lewat .well-known, dan memanfaatkan sifat stateless untuk scaling horizontal.

Roadmap: mulai dari alasan pindah ke HTTP, implementasi dengan Express dan Fastify, route handler di Next.js, kontainerisasi dengan Docker, file discovery mcp.json, lalu routing stateless dengan load balancer dan health check.

Kenapa Memindahkan Server ke HTTP

Server stdio hanya bisa dipakai host yang bisa melahirkan subprocess di mesin yang sama — editor lokal, CLI agent, atau proses Node/Python kalian. Itu tidak berlaku untuk aplikasi web, agent SaaS, atau layanan yang dipakai banyak pengguna lewat internet. Untuk itu, server perlu berbicara Streamable HTTP: request/response JSON-RPC lewat POST, plus SSE untuk notifikasi dan streaming.

Info

Kabar baiknya: keputusan transport tidak mengubah logika tool kalian. Server yang sama di episode 6 bisa dipindahkan dari StdioServerTransport ke StreamableHTTPServerTransport tanpa mengubah satu pun tool.

Keuntungan utama HTTP: reachable dari mana saja, mudah diautentikasi (episode 10), dan — berkat core stateless — bisa direplikasi tanpa sesi.

Deploy dengan Express atau Fastify

Pola dasarnya sama untuk semua framework: pasang transport HTTP sekali, lalu serahkan request POST ke transport.

JSserver-express.mjs
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
 
const app = express();
app.use(express.json());
 
const server = new McpServer({ name: "remote-tools", version: "1.0.0" });
const transport = new StreamableHTTPServerTransport({ enableJsonResponse: true });
await server.connect(transport);
 
app.post("/mcp", async (req, res) => {
  await transport.handleRequest(req.body, req, res);
});
 
app.get("/healthz", (_req, res) => {
  res.json({ status: "ok" });
});
 
app.listen(3001, () => console.log("MCP server di port 3001"));

Versi Fastify hampir identik, hanya cara menulis response-nya yang berbeda:

server-fastify.ts
import Fastify from "fastify";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
 
const app = Fastify();
const server = new McpServer({ name: "remote-tools", version: "1.0.0" });
const transport = new StreamableHTTPServerTransport({ enableJsonResponse: true });
await server.connect(transport);
 
app.post("/mcp", async (request, reply) => {
  await transport.handleRequest(request.body, request.raw, reply.raw);
});
 
app.get("/healthz", async () => ({ status: "ok" }));
 
await app.listen({ port: 3001 });

Perhatikan tiga hal: route endpoint /mcp menerima semua request JSON-RPC, route /healthz untuk health check (dipakai di bagian routing), dan express.json() (atau middleware sejenis) wajib agar body ter-parse sebelum masuk transport.

Deploy dengan Next.js Route Handler

Di Next.js App Router, endpoint MCP ditulis sebagai route handler di app/api/mcp/route.ts. Transport HTTP dibuat lazy — sekali saja — lalu diulang untuk setiap request:

app/api/mcp/route.ts
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
 
let transport;
 
export async function POST(request) {
  if (!transport) {
    transport = new StreamableHTTPServerTransport();
    await server.connect(transport);
  }
  return transport.handleRequest(await request.json(), request, Response);
}

Catatan penting: App Router mendukung streaming, tapi tidak semua opsi SSE berlaku di serverless function dengan durasi eksekusi terbatas. Untuk skala besar, jalankan server di container (bukan serverless) supaya streaming dan MRTR bekerja dengan nyaman.

Kontainerisasi dengan Docker

Multi-stage Dockerfile menjaga image tetap kecil: tahap build untuk compile TypeScript, tahap runtime hanya berisi artefak dan dependency produksi.

Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
 
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3001
CMD ["node", "dist/server.js"]

Jalankan dengan compose, lengkap dengan health check:

docker-compose.yaml
services:
  mcp-server:
    build: .
    ports:
      - "3001:3001"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3001/healthz"]
      interval: 30s

Variabel port dan konfigurasi lain dibaca lewat environment variable (misalnya variabel MCP_SERVER_PORT) yang dipetakan dari secrets di platform orkestrasi — bukan di-hardcode di image.

Discovery via .well-known

Klien bisa menemukan server MCP secara otomatis lewat URL discovery. Publikasikan file mcp.json di path /.well-known/mcp.json:

.well-known/mcp.json
{
  "mcpServers": {
    "production": {
      "url": "https://mcp.example.com/mcp"
    }
  }
}

Klien cukup tahu domain (misalnya https://mcp.example.com), membaca file tersebut, lalu mengarahkan koneksi ke url yang tercantum. Ini pola yang sama dengan /.well-known/ milik OpenID Connect — familiar dan mudah diadopsi tooling.

Stateless, Load Balancing, dan Health Check

Ini bagian paling menarik. Karena core 2026-07-28 adalah stateless, server tidak menyimpan sesi di memori — kecuali state in-flight yang sengaja dibuka lewat MRTR. Konsekuensinya: scaling horizontal tanpa sticky session, load balancer round-robin yang membiarkan request jatuh ke replica mana pun, dan health check /healthz untuk memberi tahu load balancer replica mana yang layak menerima traffic.

Linuxnginx.conf
upstream mcp_pool {
  server 10.0.0.11:3001;
  server 10.0.0.12:3001;
}
 
server {
  listen 443 ssl;
  location /mcp {
    proxy_pass http://mcp_pool;
    proxy_http_version 1.1;
  }
  location /healthz {
    proxy_pass http://mcp_pool;
  }
}

Untuk flow MRTR yang butuh follow-up diarahkan ke instance yang sama, gunakan routingId (header Mcp-Routing-Id) sebagai dasar routing di load balancer — misalnya hash berdasarkan header tersebut. Sisanya, biarkan beban tersebar merata.

Penutup

Episode 9 mengubah server lokal kalian menjadi layanan produksi: endpoint Streamable HTTP di Express, Fastify, dan Next.js, image Docker yang siap naik, discovery lewat mcp.json, serta arsitektur stateless yang membuka scaling horizontal tanpa sticky session.

Inti yang harus dibawa pulang:

  • Transport bisa diganti tanpa menyentuh tool: dari stdio ke HTTP cukup menukar transport.
  • Pola endpoint konsisten: POST /mcp untuk JSON-RPC, GET /healthz untuk kesehatan.
  • Docker multi-stage menjaga image kecil dan reproducible.
  • Discovery otomatis lewat /.well-known/mcp.json memudahkan onboarding klien.
  • Stateless = scalable: round-robin load balancer plus health check sudah cukup untuk beban besar.

Di episode 10 berikutnya kita kunci pintu yang selama ini terbuka: Authorization dengan OAuth 2.1 — peran resource server dan authorization server, header Mcp-Authorization, refresh token, PKCE, hingga mitigasi SSRF dan CRLF. Sampai jumpa!