Kenapa Perlu Membangun MCP Server Sendiri?
Cepat atau lambat, setiap tim yang bereksperimen dengan AI agent akan menabrak tembok yang sama: modelnya pintar, tapi tidak tahu apa-apa soal pelanggan, invoice, atau status layanan milik Anda. Model Context Protocol (MCP) adalah standar terbuka untuk menutup celah itu. Spesifikasinya menggambarkan MCP sebagai protokol yang menghubungkan aplikasi LLM dengan sumber data dan tool eksternal, memakai pesan JSON-RPC 2.0 di antara tiga peran: host (aplikasi LLM), client (konektor di dalam host), dan server (layanan yang menyediakan konteks dan kapabilitas). Sebuah server bisa menawarkan tiga jenis fitur: resources, prompts, dan tools.
Sebelum mulai, ada baiknya melihat repositori resmi modelcontextprotocol/servers. Di sana ada reference server seperti Filesystem, Git, Fetch, dan Memory, tetapi repositori tersebut menegaskan bahwa semuanya adalah contoh edukatif untuk mendemonstrasikan penggunaan SDK, bukan solusi siap produksi. Anda diharapkan mengevaluasi threat model sendiri. Itulah fokus panduan ini: membangun server kecil yang dirancang dengan sengaja di atas data bisnis sungguhan. Contoh yang kita pakai adalah database billing milik sebuah perusahaan hosting.
1. Menentukan Apa yang Diekspos: Read-Only Dulu, Write Belakangan
Kesalahan paling umum adalah memulai dengan tool generik seperti run_sql atau call_api. Kelihatannya fleksibel, tapi itu sama saja menyerahkan seluruh permukaan sistem Anda ke model, dan Anda tidak lagi bisa menalar apa yang sebenarnya bisa dilakukan agent.
Mulailah dari pertanyaan yang setiap minggu diajukan ke tim Anda, lalu buat satu tool sempit untuk setiap pertanyaan. Tool di MCP bersifat model-controlled: model menemukan dan memanggilnya sendiri berdasarkan percakapan. Spesifikasi menyebutkan bahwa SEHARUSNYA (SHOULD) selalu ada manusia yang bisa menolak pemanggilan tool, tetapi server Anda tidak bisa memastikan setiap client benar-benar menerapkannya. Jadi, rancang seolah-olah agent akan memanggil apa pun yang Anda ekspos.
| Fase | Yang diekspos | Contoh billing |
|---|---|---|
| 1 | Lookup read-only yang sempit | get_invoice, search_invoices, get_service_status |
| 2 | Agregat read-only | summarize_overdue_by_month |
| 3 | Write berdampak kecil dan bisa dibatalkan | add_internal_note |
| 4 | Write berdampak besar (setelah ada data audit) | issue_refund, suspend_service |
Perhatikan bahwa reference server PostgreSQL yang kini diarsipkan juga dibatasi pada akses database read-only plus inspeksi skema. Read-only adalah default yang waras. Pindah ke fase berikutnya hanya setelah audit log menunjukkan bagaimana agent benar-benar memakai tool yang sudah ada.
2. Setup Proyek dengan SDK Resmi
SDK MCP resmi tersedia untuk TypeScript, Python, Go, Java, Kotlin, C#, PHP, Ruby, Rust, dan Swift. Contoh di bawah memakai SDK TypeScript dengan Zod untuk skema:
mkdir billing-mcp && cd billing-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
Kerangka server minimum sangat kecil: membuat server, mendaftarkan tool, lalu menghubungkan transport. Untuk server lokal dengan satu pengguna, stdio adalah transport paling sederhana:
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "billing-mcp",
version: "0.1.0",
});
// tools are registered here (see next section)
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("billing-mcp running on stdio"); // stderr, never stdout
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Ada satu detail yang menjebak hampir semua orang: pada transport stdio, stdout adalah kanal protokol. Satu console.log nyasar saja bisa merusak aliran JSON-RPC, jadi arahkan log ke stderr. Untuk mencoba server, compile lalu daftarkan di client. Repositori servers menunjukkan format konfigurasi Claude Desktop:
{
"mcpServers": {
"billing": {
"command": "node",
"args": ["/path/to/billing-mcp/build/index.js"],
"env": { "BILLING_DB_URL": "postgres://mcp_readonly@localhost/billing" }
}
}
}
Catatan praktis: spesifikasi MCP terus berkembang. Artikel ini mengikuti revisi 2025-06-18, sementara repositori spesifikasi sudah menerbitkan revisi skema yang lebih baru. Kunci versi SDK Anda dan cek versi protokol yang dinegosiasikan sebelum upgrade.
3. Mendefinisikan Tool: Skema, Deskripsi, dan Kenapa Deskripsi Adalah Interface
Menurut spesifikasi, definisi tool terdiri dari name, title (opsional), description, inputSchema (JSON Schema untuk parameter), outputSchema (opsional), dan annotations (opsional). Berikut contoh tool yang nyata:
server.registerTool(
"search_invoices",
{
title: "Search invoices",
description:
"Search one customer's invoices in the billing system. Use this when the user asks about " +
"unpaid bills, payment history, or whether a specific invoice was paid. Returns at most " +
"`limit` invoices, newest first, amounts in IDR. Does NOT return line items; call " +
"get_invoice with an invoice id for those.",
inputSchema: {
customerId: z.string().describe("Internal customer ID, e.g. CUST-1042"),
status: z.enum(["unpaid", "paid", "overdue"]).optional()
.describe("Filter by payment status. Omit to include all statuses."),
limit: z.number().int().min(1).max(25).default(10)
.describe("Maximum number of invoices to return (1-25)."),
},
outputSchema: {
invoices: z.array(z.object({
id: z.string(),
issuedAt: z.string(),
dueAt: z.string(),
amountIdr: z.number(),
status: z.string(),
})),
totalMatching: z.number(),
truncated: z.boolean(),
},
annotations: { readOnlyHint: true, openWorldHint: false },
},
handleSearchInvoices,
);
Bagi developer, deskripsi hanyalah dokumentasi. Bagi agent, deskripsi adalah interface-nya. Model memutuskan apakah akan memanggil tool Anda, dan dengan argumen apa, hampir sepenuhnya dari nama, deskripsi, dan deskripsi parameter. Deskripsi yang baik menjawab empat pertanyaan:
- Kapan tool ini dipakai (dan kapan sebaiknya memakai tool lain)?
- Apa yang dikembalikan, termasuk satuan, urutan, dan batasnya?
- Apa yang tidak dilakukan, supaya agent tidak berharap mendapat line item yang memang tidak pernah dikirim?
- Seperti apa format argumennya, lengkap dengan contoh ID yang konkret?
Batasi input lewat skema, bukan lewat kalimat. enum untuk status dan max(25) untuk limit mencegah banyak jenis panggilan yang keliru sekaligus. Perlakukan perubahan deskripsi seperti perubahan API: di-review, diberi versi, dan diuji dengan agent sungguhan.
4. Mengembalikan Hasil yang Benar-Benar Bisa Dipakai Agent
Agent membayar setiap token yang Anda kembalikan, dan banjir baris data mentah justru membuat kinerjanya memburuk. Kembalikan hanya field yang dibutuhkan pertanyaan, batasi ukurannya, dan beri tahu agent jika data terpotong.
Revisi 2025-06-18 menambahkan structured tool output. Hasil tool bisa membawa structuredContent berupa objek JSON, dan jika Anda mendeklarasikan outputSchema, server WAJIB (MUST) mengembalikan hasil terstruktur yang sesuai skema tersebut. Demi kompatibilitas mundur, tool yang mengembalikan konten terstruktur SEHARUSNYA juga menyertakan JSON yang sudah diserialisasi di blok teks:
async function handleSearchInvoices({ customerId, status, limit }) {
const customer = await db.findCustomer(customerId);
if (!customer) {
return {
isError: true,
content: [{
type: "text",
text: `No customer with ID ${customerId}. IDs look like CUST-1042; ` +
`use find_customer to look one up by email first.`,
}],
};
}
const { rows, total } = await db.searchInvoices({ customerId, status, limit });
const result = {
invoices: rows.map((r) => ({
id: r.id,
issuedAt: r.issued_at,
dueAt: r.due_at,
amountIdr: r.amount,
status: r.status,
})),
totalMatching: total,
truncated: total > rows.length,
};
return {
content: [{ type: "text", text: JSON.stringify(result) }],
structuredContent: result,
};
}
Spesifikasi membedakan dua jenis error. Protocol error adalah error JSON-RPC standar untuk tool yang tidak dikenal, argumen tidak valid, atau kegagalan server. Tool execution error dikembalikan di dalam hasil dengan isError: true, untuk kegagalan API, data input yang tidak valid, atau masalah logika bisnis. Jenis kedua ini terlihat oleh model, jadi tulislah untuk model: jelaskan apa yang salah dan apa yang sebaiknya dicoba berikutnya. Pesan "Tidak ada pelanggan dengan ID X; pakai find_customer dulu" membantu agent pulih. "Error 500" tidak.
Satu nuansa praktis: jika tool mendeklarasikan outputSchema, kembalikan hasil error sebagai konten teks biasa tanpa structuredContent. Issue tracker SDK menunjukkan bahwa sebagian versi client tetap memvalidasi structuredContent terhadap output schema meskipun hasilnya error, sehingga pesan yang tadinya membantu bisa berubah menjadi kegagalan validasi.
5. Checklist Keamanan
Spesifikasi menegaskan bahwa MCP membuka jalur akses data dan eksekusi kode yang powerful, dan bahwa tool mewakili eksekusi kode arbitrer yang harus diperlakukan dengan hati-hati. Di sisi server, spesifikasi menyebutkan server WAJIB memvalidasi semua input tool, menerapkan kontrol akses yang tepat, membatasi laju (rate limit) pemanggilan tool, dan melakukan sanitasi output. Terjemahkan itu menjadi checklist konkret:
| Area | Yang perlu dilakukan |
|---|---|
| Otorisasi | Untuk server remote (HTTP), gunakan mekanisme otorisasi berbasis OAuth dari spesifikasi; hanya terima token yang diterbitkan untuk server Anda dan jangan pernah meneruskan token client ke API downstream. Untuk server stdio lokal, muat kredensial dari environment. |
| Scoping | Koneksikan dengan user database khusus yang hanya bisa SELECT dari view tertentu. Terapkan pembatasan per tenant atau per pelanggan di query, bukan di prompt. Pasang batas baris dan timeout yang tegas. |
| Anotasi tidak tepercaya | Anotasi seperti readOnlyHint hanyalah petunjuk. Client WAJIB menganggapnya tidak tepercaya kecuali servernya tepercaya, jadi jangan jadikan pengaman. Tegakkan read-only di level permission database. |
| Data tidak tepercaya | Field teks bebas seperti catatan pelanggan bisa berisi teks prompt injection. Sanitasi atau beri pembatas yang jelas sebelum dikembalikan. |
| Audit logging | Catat setiap panggilan: identitas pemanggil, nama tool, argumen yang sudah disamarkan, ukuran hasil, durasi, dan status error. Client SEHARUSNYA juga mencatat pemakaian tool, tetapi simpan jejak audit Anda sendiri. |
Penutup
MCP server pertama Anda tidak perlu canggih. Yang dibutuhkan adalah server yang sempit, read-only, terdeskripsi dengan baik, hemat token, dan terkunci di level database. Mulailah dari dua atau tiga lookup yang setiap hari masih dijawab manual oleh tim Anda, pantau audit log selama beberapa minggu, dan baru setelah itu pertimbangkan operasi write pertama. Jika Anda butuh bantuan hosting atau membangun integrasi internal seperti ini, itulah pekerjaan yang kami tangani di katili.dev.