Tutorial MCP TypeScript 2026: Bangun Server AI Tools yang Production-Ready
Pelajari cara membangun MCP server dengan TypeScript dari nol hingga siap production: arsitektur, validasi schema, error handling, best practices, dan contoh kode lengkap yang bisa langsung dijalankan.
Tutorial MCP TypeScript 2026: Bangun Server Tooling AI yang Production-Ready
1) Introduction — What & Why
Kalau kamu mengikuti tren developer di 2026, satu pola besar kelihatan jelas: agentic workflow lagi naik daun. Di GitHub Trending, banyak repo bertema AI coworker, coding harness, dan managed agents. Artinya, developer bukan cuma butuh model LLM yang pintar, tapi juga butuh cara standar untuk menghubungkan model ke tools, data, dan workflow nyata.
Di sinilah Model Context Protocol (MCP) jadi relevan.
Bayangin MCP seperti USB-C untuk ekosistem AI. Sebelum USB-C, tiap perangkat punya kabel sendiri-sendiri. Ribet, adaptor di mana-mana. Dengan USB-C, perangkat beda brand bisa “ngobrol” pakai port yang konsisten. MCP punya peran yang mirip: aplikasi AI (client) dan layanan eksternal (server) bisa berkomunikasi dengan pola yang seragam.
Kenapa ini penting buat kamu sebagai developer?
- Kamu bisa expose business logic internal (misalnya data produk, task tracker, knowledge base) sebagai tools untuk AI assistant.
- Kamu bisa bikin integrasi yang lebih maintainable dibanding “plugin custom” yang formatnya berubah-ubah.
- Kamu bisa mengontrol security boundary dengan lebih jelas (apa tool yang boleh dipanggil, input schema, validasi, timeout, audit logging).
Di tutorial ini, kita akan bikin MCP server pakai TypeScript yang benar-benar runnable, dengan:
- struktur project modern,
- error handling yang proper,
- validasi input,
- pemisahan service layer,
- dan best practice untuk production.
Fokus use case kita: Task Management MCP Server (create/list/complete task) supaya gampang dipahami tapi tetap realistis.
2) Prerequisites
Sebelum mulai, pastikan kamu punya:
- Node.js 20+ (disarankan LTS terbaru)
- npm atau pnpm
- Pemahaman dasar TypeScript (interface, async/await)
- Pemahaman dasar JSON-RPC/HTTP concepts (opsional, tapi membantu)
- Editor seperti VS Code
Dependensi utama yang kita pakai:
@modelcontextprotocol/server(SDK server MCP)zod(schema validation)pino(structured logging)dotenv(env config)
Catatan tren 2026: ekosistem MCP TypeScript bergerak cepat. Selalu cek versi stabil yang direkomendasikan di dokumentasi resmi sebelum deploy production.
3) Core Concepts (pakai analogi sederhana)
Agar gampang dicerna, kita pakai analogi restoran pintar:
- Client MCP = pelayan (AI app) yang menerima permintaan user.
- MCP Server = dapur yang tahu cara mengeksekusi aksi.
- Tools = menu aksi (buat task, lihat task, tandai selesai).
- Resources = bahan/data yang bisa dibaca.
- Prompts = template instruksi siap pakai.
Ketika user minta, “Tolong bikin task deploy staging besok,” client akan memilih tool yang relevan, kirim parameter sesuai schema, lalu server mengeksekusi logic.
Kenapa schema penting?
Tanpa schema, AI bisa ngirim input ngawur. Dengan zod, kita batasi input sejak awal:
- string tidak boleh kosong,
- priority hanya
low|medium|high, - dueDate harus format valid.
Jadi risiko bug dan perilaku aneh turun drastis.
4) Architecture / Diagram
Arsitektur sederhana yang akan kita bangun:
+-------------------+ JSON-RPC / MCP +--------------------------+ | MCP Client | ---------------------------> | MCP Server (TypeScript) | | (Claude/ChatGPT/ | | | | IDE Agent/etc) | <--------------------------- | Tool Handlers | +-------------------+ | - create_task | | - list_tasks | | - complete_task | +------------+-------------+ | v +--------------------------+ | TaskService | | - validation | | - business rules | | - in-memory store | +--------------------------+
Untuk tutorial ini, data disimpan in-memory supaya fokus ke konsep MCP. Di production, tinggal ganti storage ke PostgreSQL/Redis tanpa ubah kontrak tool.
5) Step-by-Step Implementation (Complete Runnable Code)
Step A — Inisialisasi project
mkdir mcp-task-server cd mcp-task-server npm init -y npm i @modelcontextprotocol/server zod pino dotenv npm i -D typescript tsx @types/node npx tsc --init
Update package.json:
{ "name": "mcp-task-server", "version": "1.0.0", "type": "module", "scripts": { "dev": "tsx src/server.ts", "build": "tsc -p tsconfig.json", "start": "node dist/server.js" } }
Update tsconfig.json minimal:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "outDir": "dist", "rootDir": "src" }, "include": ["src"] }
Buat file .env:
LOG_LEVEL=info NODE_ENV=development
Step B — Buat model dan service layer
Buat src/task-service.ts:
import { randomUUID } from "node:crypto"; import { z } from "zod"; export const CreateTaskInputSchema = z.object({ title: z.string().min(3, "title minimal 3 karakter"), description: z.string().optional().default(""), priority: z.enum(["low", "medium", "high"]).default("medium"), dueDate: z.string().datetime().optional() }); export const CompleteTaskInputSchema = z.object({ id: z.string().uuid("id task harus UUID valid") }); export type Task = { id: string; title: string; description: string; priority: "low" | "medium" | "high"; dueDate?: string; completed: boolean; createdAt: string; completedAt?: string; }; export class TaskService { private readonly tasks = new Map<string, Task>(); createTask(rawInput: unknown): Task { const input = CreateTaskInputSchema.parse(rawInput); const task: Task = { id: randomUUID(), title: input.title, description: input.description, priority: input.priority, dueDate: input.dueDate, completed: false, createdAt: new Date().toISOString() }; this.tasks.set(task.id, task); return task; } listTasks(includeCompleted = true): Task[] { const all = [...this.tasks.values()]; return includeCompleted ? all : all.filter((t) => !t.completed); } completeTask(rawInput: unknown): Task { const input = CompleteTaskInputSchema.parse(rawInput); const task = this.tasks.get(input.id); if (!task) { throw new Error(`Task dengan id ${input.id} tidak ditemukan`); } if (task.completed) { return task; } const updated: Task = { ...task, completed: true, completedAt: new Date().toISOString() }; this.tasks.set(updated.id, updated); return updated; } }
Step C — Buat MCP server + tool handlers
Buat src/server.ts:
import "dotenv/config"; import pino from "pino"; import { McpServer } from "@modelcontextprotocol/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/server/stdio.js"; import { z } from "zod"; import { TaskService } from "./task-service.js"; const logger = pino({ level: process.env.LOG_LEVEL ?? "info" }); const taskService = new TaskService(); const server = new McpServer({ name: "task-manager-mcp", version: "1.0.0" }); server.tool( "create_task", { title: z.string().min(3), description: z.string().optional(), priority: z.enum(["low", "medium", "high"]).optional(), dueDate: z.string().datetime().optional() }, async (args) => { try { const task = taskService.createTask(args); return { content: [ { type: "text", text: `✅ Task dibuat: ${task.title} (id=${task.id}, priority=${task.priority})` } ] }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error({ err: error }, "Gagal create_task"); return { isError: true, content: [{ type: "text", text: `❌ create_task gagal: ${message}` }] }; } } ); server.tool( "list_tasks", { includeCompleted: z.boolean().optional() }, async (args) => { try { const tasks = taskService.listTasks(args.includeCompleted ?? true); if (tasks.length === 0) { return { content: [{ type: "text", text: "📭 Belum ada task." }] }; } const lines = tasks.map( (t, i) => `${i + 1}. [${t.completed ? "x" : " "}] ${t.title} | ${t.priority} | id=${t.id}` ); return { content: [{ type: "text", text: lines.join(" ") }] }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error({ err: error }, "Gagal list_tasks"); return { isError: true, content: [{ type: "text", text: `❌ list_tasks gagal: ${message}` }] }; } } ); server.tool( "complete_task", { id: z.string().uuid() }, async (args) => { try { const task = taskService.completeTask(args); return { content: [ { type: "text", text: `🎉 Task selesai: ${task.title} (completedAt=${task.completedAt})` } ] }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error({ err: error }, "Gagal complete_task"); return { isError: true, content: [{ type: "text", text: `❌ complete_task gagal: ${message}` }] }; } } ); async function main() { const transport = new StdioServerTransport(); process.on("uncaughtException", (err) => { logger.fatal({ err }, "Uncaught exception"); process.exit(1); }); process.on("unhandledRejection", (reason) => { logger.fatal({ reason }, "Unhandled rejection"); process.exit(1); }); await server.connect(transport); logger.info("MCP Task Server berjalan via STDIO"); } main().catch((err) => { logger.fatal({ err }, "Gagal menjalankan server"); process.exit(1); });
Step D — Jalankan server
npm run dev
Kalau sukses, server akan siap dipanggil oleh MCP client yang kamu konfigurasi (misalnya desktop assistant, IDE plugin, atau orchestration layer internal).
Step E — Contoh penggunaan praktis
Skenario nyata:
- User: “Bikin task deploy API jam 5 sore, prioritas tinggi.”
- Client memilih tool
create_task. - Server validasi input dan simpan task.
- User minta “List task yang belum selesai.”
- Client panggil
list_tasksdenganincludeCompleted=false.
Dari sisi engineering, ini jauh lebih rapi daripada hardcode function calling tanpa contract yang konsisten.
6) Best Practices (industry-proven)
a) Pisahkan protocol layer vs domain layer
server.ts hanya menangani transport/protocol. Logic bisnis ditaruh di TaskService. Ini bikin testing lebih mudah dan mencegah file server jadi “God object”.
b) Validasi semua input tool
Anggap input dari model adalah untrusted. Selalu parse dengan schema validator (zod, valibot, dsb).
c) Structured logging
Gunakan logger JSON (pino) supaya gampang dianalisis di ELK/Grafana/Datadog.
d) Timeout & retry policy
Kalau tool kamu akses API eksternal, kasih timeout jelas + retry terbatas dengan backoff. Jangan biarkan tool nge-hang lama.
e) Principle of least privilege
Jangan expose tool yang terlalu “powerful” tanpa guardrail. Misalnya tool delete massal harus butuh konfirmasi eksplisit.
f) Versioning kontrak
Tambahkan version di metadata server dan dokumentasikan perubahan schema agar client tidak break saat upgrade.
7) Common Mistakes (dan cara hindarinya)
1. Logging ke stdout pada STDIO transport
Untuk server STDIO, stream protocol berjalan di stdout. Logging sembarangan bisa merusak frame pesan.
Solusi: gunakan logger yang diarahkan dengan benar dan hindari console.log liar.
2. Tidak menangani error sebagai output tool
Kalau throw mentah tanpa handling, client dapat pengalaman buruk dan sulit debug.
Solusi: return isError: true dengan pesan yang jelas.
3. Schema terlalu longgar
Input tanpa batas bikin model mengirim data tidak valid.
Solusi: enforce enum, min length, format datetime/uuid.
4. Campur business logic + transport
Project cepat jadi sulit dirawat ketika semua logic numpuk di satu file.
Solusi: layering sejak awal (handlers, service, repo).
5. Tidak observability-ready
Saat incident, kamu tidak tahu tool mana yang fail, untuk input apa.
Solusi: log context penting (toolName, durationMs, status, errorCode).
8) Advanced Tips
A) Tambahkan persistence tanpa ubah kontrak tools
Buat TaskRepository interface:
export interface TaskRepository { create(task: Task): Promise<void>; findById(id: string): Promise<Task | null>; list(includeCompleted: boolean): Promise<Task[]>; update(task: Task): Promise<void>; }
Lalu implementasi PostgresTaskRepository atau RedisTaskRepository. Handler tool tetap sama.
B) Idempotency key untuk operasi create
Untuk mencegah task dobel saat retry, terima idempotencyKey dan simpan mapping request→result.
C) Authorization per tool
Kalau ini dipakai lintas tim, tambahkan guard:
- role user,
- namespace project,
- scope tool (
task:write,task:read).
D) Add metrics
Expose metrik:
mcp_tool_calls_total{tool,status}mcp_tool_duration_ms{tool}mcp_tool_errors_total{tool,errorType}
Ini krusial untuk capacity planning saat trafik naik.
E) Contract tests
Bikin test yang memastikan output tool tetap sesuai shape lama meski internal logic berubah. Ini mencegah regression di client AI.
9) Summary & Next Steps
Kita sudah membangun MCP server TypeScript yang:
- punya 3 tools nyata (
create_task,list_tasks,complete_task), - validasi input ketat,
- error handling yang aman,
- arsitektur bersih dan siap dikembangkan.
Next steps yang saya sarankan:
- Ganti in-memory storage ke PostgreSQL.
- Tambah autentikasi + otorisasi.
- Tambah observability (metrics + tracing).
- Deploy sebagai service terpisah (containerized).
- Integrasikan ke client AI yang kamu pakai di workflow harian.
Kalau kamu konsisten pakai pattern ini, MCP server kamu nggak cuma demo, tapi siap jadi fondasi automasi AI internal yang reliable.
10) References
- MCP Introduction (Official): https://modelcontextprotocol.io/introduction
- MCP Build Server Guide (Official): https://modelcontextprotocol.io/quickstart/server
- MCP TypeScript SDK (Official Repo): https://github.com/modelcontextprotocol/typescript-sdk
- MCP Specification: https://spec.modelcontextprotocol.io
- MCP Servers Examples: https://github.com/modelcontextprotocol/servers
- GitHub Trending (indikasi topik agent tooling): https://github.com/trending
- GitHub Trending TypeScript: https://github.com/trending/typescript?since=daily
- Medium Software Engineering tag (tren diskusi harness/agent architecture): https://medium.com/tag/software-engineering
Catatan Riset Singkat (untuk konteks pembaca)
- Topik agentic tooling dan AI coworker terlihat dominan di trending repo saat riset harian.
- Ekosistem MCP makin luas didukung berbagai client dan IDE.
- Fokus engineering 2026 bergeser dari “model saja” ke harness, protocol, observability, dan governance.