Kalau kamu pernah pakai Claude Desktop atau Cursor, kemungkinan besar kamu sudah pernah lihat MCP server beraksi. Itu cara AI client nemuin dan manggil tool eksternal, baca file, query database, atau hit REST API, semua lewat satu protokol standar. Bikin sendiri ternyata jauh lebih gampang dari yang kelihatannya. Dalam 30 menit, kamu bisa punya MCP server jalan yang ditulis pakai TypeScript, expose beberapa tool custom, dan tersambung ke Claude Desktop buat testing.
Ini walkthrough hands-on. Di akhir, kamu akan paham tiga primitif MCP (tools, resources, prompts), gimana JSON-RPC 2.0 bawa pesan antara client dan server, dan gimana nge-ship server yang bisa di-install developer lain pakai npx.
Prasyarat
- Node.js 20+ (cek pakai
node --version) npm10+- Claude Desktop terinstall (buat bagian testing), atau MCP client lain yang kompatibel
- Sekitar 5 menit waktu luang di disk
Spesifikasi resmi ada di modelcontextprotocol.io. SDK TypeScript-nya di github.com/modelcontextprotocol/typescript-sdk.
MCP itu sebenarnya apa
MCP adalah protokol berbasis JSON-RPC 2.0 di mana aplikasi host (Claude Desktop, IDE, chat app) menanamkan client yang ngobrol sama satu atau lebih server. Tiap server expose tiga jenis capability:
- Tools: fungsi yang bisa dipanggil LLM (mirip OpenAI function calling, tapi sudah standar)
- Resources: data read-only yang bisa di-fetch client dan disuntikkan ke context (file, row DB, response API)
- Prompts: template prompt reusable yang bisa ditrigger user pakai parameter
Transport-nya bisa stdio (server jalan sebagai child process, komunikasi lewat stdin/stdout — pattern paling umum buat local dev) atau Streamable HTTP (server jalan sebagai HTTP service biasa buat remote atau shared use). MCP awalnya pakai HTTP+SSE di akhir 2024, terus nambahin Streamable HTTP transport di revisi spec 2025-03-26, jadi kode yang kamu temuin dari sebelum itu biasanya sudah outdated.
Step 1: Scaffold project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Inisialisasi TypeScript:
npx tsc --init
Edit tsconfig.json set "outDir": "./build" dan "rootDir": "./src", terus tambahin field "bin" di package.json biar server bisa diluncurin pakai npx:
{
"name": "my-mcp-server",
"version": "0.1.0",
"type": "module",
"bin": {
"my-mcp-server": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod +x build/index.js"
}
}
Step 2: Server minimal
Bikin src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "my-mcp-server",
version: "0.1.0",
});
// Tool simpel: echo balik string
server.tool(
"echo",
"Balikin apa pun yang user kasih",
{
message: { type: "string", description: "Text yang mau di-echo" },
},
async ({ message }) => ({
content: [{ type: "text", text: `Echo: ${message}` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
Build dan jalanin:
npm run build
node build/index.js
Kalau kamu jalanin ini, gak ada yang keliatan — server cuma nunggu pesan JSON-RPC di stdin. Itu bener. Lanjut, kita sambungin client beneran ke sini.
Step 3: Sambungin ke Claude Desktop
Buka ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) atau %APPDATA%\Claude\claude_desktop_config.json (Windows). Tambahin server kamu:
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/build/index.js"]
}
}
}
Restart Claude Desktop. Harus muncul icon palu kecil di chat input. Klik, dan tool echo kamu bakal muncul. Minta Claude "pakai tool echo buat bilang halo" — dia akan manggil server kamu, dapet Echo: halo, terus balikin itu.
Step 4: Tambahin tool beneran dengan validasi input
Tool yang gak ngapa-ngapain cukup buat 5 menit pertama. Bagian serunya itu nyambungin ke sesuatu yang beneran kepake. Ini tool yang nampilin list file di directory yang diizinin user:
import { z } from "zod";
import { readdir } from "node:fs/promises";
import { resolve } from "node:path";
server.tool(
"list_files",
"List file di sebuah directory (read-only, max 50 entry)",
{
path: z.string().describe("Path absolut ke sebuah directory"),
},
async ({ path }) => {
const safe = resolve(path);
const entries = await readdir(safe, { withFileTypes: true });
const names = entries
.slice(0, 50)
.map((e) => `${e.isDirectory() ? "📁" : "📄"} ${e.name}`)
.join("\n");
return {
content: [{ type: "text", text: names || "(directory kosong)" }],
};
}
);
Schema Zod punya dua fungsi sekalian: validasi input DAN ngehasilin JSON Schema yang dikirim ke LLM sebagai bagian dari definisi tool. LLM baca schema itu buat nentuin argumen mana yang harus dikasih.
Step 5: Expose resource
Resource itu bisa di-address pakai URI. Pattern yang umum adalah file:// buat file lokal atau scheme custom buat data kamu sendiri. Ini resource yang balikin config statis:
server.resource(
"config",
"config://app",
async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({ env: "dev", version: "0.1.0" }, null, 2),
},
],
})
);
Client bisa list resource pakai resources/list dan baca satu pakai resources/read. Di Claude Desktop, resource ini otomatis ditempel ke percakapan kalau user nyebut di chat.
Step 6: Tambahin prompt template
Prompt itu template pesan berparameter yang bisa ditrigger user dari UI client. Contoh:
server.prompt(
"review-code",
"Minta model buat review potongan kode buat cari bug",
{ code: z.string() },
({ code }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Tolong review kode ini buat bug, masalah security, dan clarity:\n\n\`\`\`\n${code}\n\`\`\``,
},
},
],
})
);
Step 7: Biar bisa di-ship
Tambahin README.md yang nunjukin cara install dan konfigurasi. Terus siapa pun bisa jalanin pakai:
npx my-mcp-server
Buat Streamable HTTP transport (berguna buat server yang di-share), tinggal ganti transport-nya:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.use(express.json());
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // atau function buat session stateful
});
app.post("/mcp", async (req, res) => {
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => console.log("MCP server di http://localhost:3000/mcp"));
Catatan: StreamableHTTPServerTransport butuh express yang diinstall terpisah (npm install express), dan di production kamu pasti mau nambahin autentikasi, karena siapa pun yang bisa nyampe ke endpoint bisa drive tool kamu.
Tips debugging
- Pakai MCP Inspector buat test server kamu tanpa client beneran:
npx @modelcontextprotocol/inspector node build/index.js - Log ke
stderr, janganstdout— stdout udah dipesan buat pesan JSON-RPC, text lain bakal ngebrek stdio transport - Kalau Claude Desktop bilang server kamu gagal start, cek log di
~/Library/Logs/Claude/mcp.log(macOS) - Buat HTTP transport, inspector di
/mcpbakal nunjukin JSON request/response lengkap
Kapan MCP masuk akal (dan kapan enggak)
MCP pas dipakai kalau:
- Kamu mau MCP client apapun (Claude Desktop, Cursor, Continue, custom) pake integrasi yang sama
- Kamu lagi bikin integrasi reusable yang mau di-publish (satu server, banyak host)
- Kamu mau LLM ngedeteksi capability secara runtime, bukan hardcode list tool
Ini overkill kalau:
- Kamu cuma target satu SDK LLM tertentu dan satu app aja
- Integrasi-nya prototype one-off
- Kamu butuh streaming response balik ke LLM di tengah panggilan (MCP tool balikin satu result per call, walaupun bisa gede)
Untuk kebanyakan production agent hari ini, MCP itu default yang bener. Ekosistem udah punya server buat Postgres, GitHub, Notion, Slack, Sentry, dan puluhan lainnya. Bikin sendiri ya kerjaan 30 menit, asal kamu udah liat pattern-nya.