← Kembali ke Blog

Publish MCP Server Remote pakai TypeScript: Dari Local Dev sampai Registry Resmi

Tutorial Python di blog ini ngebahas MCP server lokal. Berguna, tapi ninggalin bagian yang sulit: momen server kamu harus bisa dijangkau dari mana aja, bukan cuma laptop sendiri.

Remote MCP server sebenernya cuma HTTP service biasa. Dia ngomong JSON-RPC 2.0 lewat streamable HTTP (transport yang direkomendasiin spec, gantiin SSE lama di revisi 2025-11-25). Semua client MCP yang support remote bisa ngomong: Claude Code, Claude Desktop, Cursor, Cline, OpenAI Agents SDK, dan daftar yang terus tumbuh. Bikin sekali, ship sekali, semua client dapet.

Artikel ini bikin remote MCP server di TypeScript pake SDK resmi @modelcontextprotocol/sdk (versi 1.29.0 pas nulis ini), tambah OAuth 2.1, test pake MCP Inspector, terus publish ke MCP Registry resmi supaya developer lain bisa nemuin.

Prasyarat

  • Node.js 20+ (node --version)
  • npm atau pnpm
  • Akun Cloudflare (atau host lain yang jalanin Node server, contoh deploy pake Workers)
  • Akun GitHub buat langkah publish ke MCP Registry
  • Sekitar 20 menit

Langkah 1: Scaffold Project

mkdir mcp-remote-demo && cd mcp-remote-demo
npm init -y
npm install @modelcontextprotocol/sdk express zod
npm install -D typescript @types/node @types/express tsx

Ini narik TypeScript SDK resmi, Express buat HTTP layer, dan Zod buat validasi input (SDK pake Zod schema buat generate JSON Schema untuk input tool). tsx jalanin TypeScript langsung pas development.

tsconfig.json minimal:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "dist"
  },
  "include": ["src"]
}

Langkah 2: Nulis Server

Todo MCP server lebih berguna daripada calculator. Dia bikin model bisa list, bikin, dan selesaikan task, yang emang kayak gitu sih kerja agent beneran.

// src/server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const todos: Array<{ id: number; title: string; done: boolean }> = [];
let nextId = 1;

const server = new McpServer({
  name: 'todo-server',
  version: '0.1.0',
});

server.tool(
  'add_todo',
  'Create a new todo item and return its id',
  { title: z.string().min(1).max(200) },
  ({ title }) => {
    const todo = { id: nextId++, title, done: false };
    todos.push(todo);
    return {
      content: [{ type: 'text', text: `Created todo #${todo.id}: ${todo.title}` }],
    };
  }
);

server.tool(
  'list_todos',
  'List all todos, optionally filtered by completion state',
  { onlyOpen: z.boolean().default(false) },
  ({ onlyOpen }) => {
    const items = todos.filter((t) => (onlyOpen ? !t.done : true));
    return {
      content: [
        {
          type: 'text',
          text: items.length === 0
            ? 'No todos.'
            : items.map((t) => `#${t.id} [${t.done ? 'x' : ' '}] ${t.title}`).join('\n'),
        },
      ],
    };
  }
);

server.tool(
  'complete_todo',
  'Mark a todo as done by id',
  { id: z.number().int().positive() },
  ({ id }) => {
    const todo = todos.find((t) => t.id === id);
    if (!todo) {
      return { content: [{ type: 'text', text: `Todo #${id} not found` }], isError: true };
    }
    todo.done = true;
    return { content: [{ type: 'text', text: `Completed todo #${id}` }] };
  }
);

// Expose read-only resource: JSON dump dari todo list
server.resource(
  'todo-list',
  'todos://all',
  async (uri) => ({
    contents: [{ uri: uri.href, text: JSON.stringify(todos, null, 2) }],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

SDK expose tiga primitif, sesuai spesifikasi MCP:

  • server.tool() daftarin fungsi yang bisa dipanggil. Model yang mutusin kapan manggil berdasarkan nama dan deskripsi.
  • server.resource() expose data read-only yang dialamatin pake URI, mirip endpoint GET.
  • server.prompt() (nggak dipake di sini) daftarin template prompt yang bisa user trigger manual.

Deskripsi tool lebih penting daripada nama fungsi. Model milih tool dengan baca deskripsi. "Mark a todo as done by id" langsung kasih tau model apa yang tool itu lakuin. "Complete" doang bikin model nebak.

Langkah 3: Pindah ke Streamable HTTP

Transport stdio jalanin server sebagai subprocess. Oke buat Claude Desktop, nggak berguna buat cloud. Spec nyarankan streamable HTTP buat server remote: satu endpoint POST /mcp yang nerima JSON-RPC, plus GET buat pesan dari server.

// src/http.ts
import express from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';

function buildServer() {
  const s = new McpServer({ name: 'todo-server', version: '0.1.0' });
  // ... daftar tools dan resources dari Langkah 2 ...
  return s;
}

const app = express();
app.use(express.json());

app.post('/mcp', async (req, res) => {
  const server = buildServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => randomUUID(),
  });
  res.on('close', () => transport.close());
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => console.log(`MCP server on http://localhost:${port}/mcp`));

Server baru tiap request itu pola yang dipake contoh SDK. Transport-nya stateful (dia track session id) tapi server-nya murah buat dibikin.

Jalanin:

npx tsx src/http.ts
# MCP server on http://localhost:3000/mcp

Langkah 4: Test Pake MCP Inspector

Inspector itu web UI yang nyambung ke server kamu dan biarin kamu utak-atik tool, resource, sama prompt secara manual. Paket npm yang sama yang dipake tim Anthropic sendiri.

npx -y @modelcontextprotocol/inspector

Buka URL yang dia cetak (biasanya http://localhost:5173), set transport ke Streamable HTTP, arahin ke http://localhost:3000/mcp, terus connect. Panel kiri nampilin daftar tools; klik add_todo, isi title, klik Run, response muncul di kanan.

Inspector juga nampilin raw JSON-RPC traffic. Kalau tool gagal, error message di situ biasanya cara paling cepet buat ngerti kenapa.

Langkah 5: Tambah OAuth 2.1

Server remote di production butuh auth. Spec mewajibin OAuth 2.1 buat transport HTTP, dengan PKCE buat public client (desktop app, CLI) dan client credentials buat machine-to-machine. Revisi November 2025 naikin ini dari rekomendasi jadi syarat autentikasi.

Setup paling simpel pake identity provider eksternal. Cloudflare Access, Auth0, sama Stytch semua jalan; SDK nyertain OAuth client referensi yang bisa kamu adaptasi. Buat tool internal single-tenant, mTLS atau static bearer token udah cukup. Buat server publik multi-tenant, OAuth satu-satunya opsi waras.

Jalan tengah yang pragmatis: biarin server private, taruh di belakang VPN atau service mesh, skip OAuth. Spec nggak butuh OAuth kalau server-nya nggak bisa dijangkau dari internet publik.

Langkah 6: Deploy ke Cloudflare Workers

Cloudflare Workers jalanin SDK dengan baik. Perubahan utama cuma ganti Express pake handler fetch Workers:

// src/worker.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';

export default {
  async fetch(request: Request): Promise<Response> {
    if (request.method !== 'POST' || new URL(request.url).pathname !== '/mcp') {
      return new Response('Not found', { status: 404 });
    }
    const server = new McpServer({ name: 'todo-server', version: '0.1.0' });
    // ... daftarin tools ...
    const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
    return transport.handleRequest(request);
  },
};
npm install -D wrangler
npx wrangler deploy

Workers kasih kamu URL HTTPS publik, edge global, dan tanpa cold-start fee di tier gratis. Kode yang sama jalan di Fly.io, Render, Railway, atau proses Node biasa di belakang nginx.

Langkah 7: Sambungin Claude Code ke Server Remote Kamu

Begitu di-deploy, daftarin server-nya ke Claude Code:

claude mcp add --transport http todo-server https://your-worker.workers.dev/mcp

Flag --transport http dibutuhin buat server remote; stdio itu default dan bakal coba spawn proses lokal. Verifikasi pake claude mcp list. Server harus muncul dengan nama, versi, dan jumlah tool-nya.

Langkah 8: Publish ke MCP Registry Resmi

MCP Registry itu direktori publik server-server MCP. Listing di situ adalah cara user Claude Desktop, Cursor, dan client lain nemuin tool pihak ketiga.

Publish butuh server.json di root repo kamu:

{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-11-25/server.schema.json",
  "name": "io.github.username-kamu/todo-server",
  "title": "Todo Server",
  "description": "Server MCP minimal yang ngatur todo list",
  "version": "0.1.0",
  "remotes": [
    {
      "type": "streamable-http",
      "url": "https://your-worker.workers.dev/mcp"
    }
  ]
}

Autentikasi ke registry pake GitHub OAuth. Panduan publish nuntun langkah-langkahnya. Kamu sign in pake akun GitHub, registry verifikasi kamu yang punya repo-nya, listing live dalam beberapa menit.

Bot narik server.json kamu berkala, jadi naikin versi cukup commit. Nggak perlu submit ulang manual.

Kapan Remote MCP Itu Pilihan yang Salah

Server remote nggak selalu jawabannya.

  • Satu user, satu client, nggak ada rencana share. Local stdio lebih simpel dan ngilangin pertanyaan auth.
  • Workflow yang sensitif latency. Streamable HTTP nambah round trip yang sebelumnya nggak dibutuhin model. Server lokal di proses yang sama lebih cepet.
  • Data sensitif yang nggak boleh keluar dari network kamu. Local stdio, atau server remote di belakang VPN private, satu-satunya opsi. Taruh PHI di Worker publik adalah insiden compliance yang nunggu kejadian.
  • Write throughput tinggi. Transport streamable HTTP di spec itu request/response. Buat push-based update, layer SSE atau webhook di atasnya.

Buat kebanyakan use case production, kalkulusnya: ship server remote kalau lebih dari satu client atau user butuh dia, dan terima overhead operasionalnya. Ship local stdio kalau nggak.

Jebakan yang Sering Kejadian

Lupa handle session cleanup. Transport yang long-lived bocor memori kalau kamu nggak panggil transport.close() pas disconnect. Contoh Express nge-wire ini ke res.on('close'). Workers handle otomatis.

Deskripsi tool yang longgar. Tool namanya process dengan deskripsi "process data" nggak kepake. Model bakal manggil dia buat apa pun dan apa pun. Jadi spesifik: sebutin input, sebutin output, sebutin side effect.

Versi SDK longgar. TypeScript SDK sering ada breaking change antara minor version. Pin: "@modelcontextprotocol/sdk": "1.29.0". Line v2 lagi alpha (2.0.0aN); v2 stabil ditargetin Juli 2026 per catatan Python SDK, dan TypeScript SDK bakal ngikutin bentuk yang sama.

Skip Inspector. Tiap tutorial MCP skip Inspector. Terus model gagal manggil tool dan developer habisin 30 menit nebak kenapa. Dua menit di Inspector langsung keliatan schema mismatch-nya.

Mau Lanjut ke Mana

Server remote yang jalan itu titik awal, bukan tujuan. Follow-up praktis, urut dari nilai terbesar:

  • Tambahin OAuth 2.1 yang bener. SDK punya helper, spec punya detailnya, dan skip-nya batasi kamu ke deployment single-tenant.
  • Tambahin sampling. Request sampling/createMessage biarin server kamu minta host LLM buat ngerjain sesuatu, yang bikin MCP server jadi agent beneran. Docs sampling ngebahas protokolnya.
  • Versi server kamu pake Server Card. Working group lagi finalisasi formatnya, tapi nambah sekarang forward-compatible.
  • Pantau daftar SEP buat perubahan protokol. Registry, MCP Apps (UI interaktif), sama Tasks (operasi long-running) semua lagi aktif dikembangin.

Protokolnya masih muda, spec-nya gerak, ekosistemnya ship tiap minggu. Kabar baiknya: tiap server yang kamu bikin hari ini bakal terus jalan pas spec berevolusi. Backward compatibility adalah salah satu hal yang project ini seriusin.

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis