MCP Server: Cara Membuat AI Tools Custom untuk Claude dan Cursor
Senin, 23 Des 2024
Pernahkah kamu berpikir, “Andai Claude bisa langsung akses database saya” atau “Kalau saja Cursor bisa fetch data dari API internal kantor”? Nah, sekarang itu semua bisa terjadi dengan MCP Server.
Model Context Protocol (MCP) adalah standar baru yang memungkinkan kamu membuat tools custom untuk AI assistant seperti Claude dan Cursor. Dengan MCP, kamu bisa extend kemampuan AI sesuai kebutuhan spesifik project-mu.
Dalam tutorial ini, kita akan build MCP Server dari nol — mulai dari setup project, membuat tool pertama, testing, sampai integrasi dengan Claude Desktop dan Cursor IDE. Semua dengan kode yang bisa langsung kamu copy-paste dan jalankan.
Apa Itu MCP Server?
MCP (Model Context Protocol) adalah protokol open-source yang dikembangkan oleh Anthropic untuk menghubungkan AI models dengan external tools dan data sources. Bayangkan MCP sebagai “USB port” untuk AI — kamu bisa colokkan berbagai tools dan AI langsung bisa menggunakannya.
Kenapa MCP Server Penting?
- Custom Tools: Buat tools sesuai kebutuhan spesifik — akses database, call internal API, manipulasi file, dll.
- Standardized Protocol: Satu server bisa digunakan di berbagai AI clients (Claude, Cursor, VSCode, dll.)
- Local First: Data sensitif tetap di local machine, tidak perlu kirim ke cloud.
- Extensible: Mudah ditambahkan tools baru tanpa rewrite seluruh server.
Arsitektur MCP
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ AI Client │────▶│ MCP Server │────▶│ External APIs │
│ (Claude/Cursor) │ │ (Your Tools) │ │ (Weather, DB) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
MCP Server berjalan sebagai process terpisah yang berkomunikasi dengan AI client melalui stdio (standard input/output). Setiap kali AI butuh menjalankan tool, ia akan send request ke MCP Server, server execute tool-nya, lalu return hasilnya ke AI.
Prerequisites
Sebelum mulai, pastikan kamu sudah menginstall:
1. Node.js (v18 atau lebih baru)
# Check versi Node.js
node --version
# Output: v18.x.x atau lebih tinggi
# Jika belum install, download dari https://nodejs.org
2. Package Manager (npm atau pnpm)
# npm sudah include dengan Node.js
npm --version
# Atau install pnpm (recommended)
npm install -g pnpm
3. Text Editor
Gunakan VSCode atau Cursor IDE untuk development experience terbaik.
4. Basic TypeScript Knowledge
Kamu perlu familiar dengan:
- TypeScript syntax dasar
- async/await
- ES modules (import/export)
Step 1: Setup Project MCP Server
Mari kita mulai dengan membuat project baru. Kita akan build weather tool sebagai contoh — tool yang bisa fetch data cuaca dari API.
1.1 Buat Directory Project
# Buat folder baru
mkdir mcp-weather-server
cd mcp-weather-server
# Initialize npm project
npm init -y
1.2 Install Dependencies
# Install MCP SDK dan TypeScript dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Penjelasan packages:
@modelcontextprotocol/sdk: Official SDK untuk membuat MCP Serverzod: Library untuk schema validation (define input/output tools)typescript: TypeScript compilertsx: TypeScript executor (untuk run TS files langsung)
1.3 Setup TypeScript Config
Buat file tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
1.4 Update package.json
Edit package.json untuk menambahkan scripts dan type module:
{
"name": "mcp-weather-server",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}
1.5 Buat Folder Structure
mkdir src
touch src/index.ts
Struktur project sekarang:
mcp-weather-server/
├── src/
│ └── index.ts
├── package.json
├── tsconfig.json
└── node_modules/
Step 2: Membuat Tool Pertama (Weather API)
Sekarang kita akan membuat MCP Server dengan satu tool: get_weather. Tool ini akan fetch data cuaca berdasarkan nama kota.
2.1 Kode Lengkap MCP Server
Buka src/index.ts dan paste kode berikut:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Schema untuk input tool get_weather
const GetWeatherSchema = z.object({
city: z.string().describe("Nama kota untuk mendapatkan data cuaca"),
});
// Tipe data untuk weather response
interface WeatherData {
city: string;
temperature: number;
condition: string;
humidity: number;
wind_speed: number;
}
// Function untuk fetch weather data
// Dalam production, ganti dengan real API seperti OpenWeatherMap
async function fetchWeather(city: string): Promise<WeatherData> {
// Simulasi API call - dalam real app, gunakan fetch ke weather API
// Contoh dengan OpenWeatherMap:
// const response = await fetch(
// `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`
// );
// Untuk demo, kita return mock data
const mockWeatherData: Record<string, WeatherData> = {
"jakarta": {
city: "Jakarta",
temperature: 32,
condition: "Partly Cloudy",
humidity: 75,
wind_speed: 12,
},
"bandung": {
city: "Bandung",
temperature: 24,
condition: "Sunny",
humidity: 60,
wind_speed: 8,
},
"surabaya": {
city: "Surabaya",
temperature: 34,
condition: "Hot",
humidity: 70,
wind_speed: 15,
},
"bali": {
city: "Bali",
temperature: 30,
condition: "Tropical",
humidity: 80,
wind_speed: 10,
},
};
const cityLower = city.toLowerCase();
if (mockWeatherData[cityLower]) {
return mockWeatherData[cityLower];
}
// Default response untuk kota yang tidak ada di mock data
return {
city: city,
temperature: Math.floor(Math.random() * 15) + 20, // 20-35°C
condition: "Clear",
humidity: Math.floor(Math.random() * 30) + 50, // 50-80%
wind_speed: Math.floor(Math.random() * 20) + 5, // 5-25 km/h
};
}
// Inisialisasi MCP Server
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
});
// Register tool: get_weather
server.tool(
"get_weather",
"Mendapatkan informasi cuaca terkini untuk sebuah kota",
GetWeatherSchema.shape,
async ({ city }) => {
try {
const weather = await fetchWeather(city);
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
data: {
city: weather.city,
temperature: `${weather.temperature}°C`,
condition: weather.condition,
humidity: `${weather.humidity}%`,
wind_speed: `${weather.wind_speed} km/h`,
},
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: `Gagal mendapatkan data cuaca: ${error}`,
}),
},
],
isError: true,
};
}
}
);
// Start server dengan stdio transport
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP Server running on stdio");
}
main().catch(console.error);
2.2 Penjelasan Kode
Mari breakdown komponen-komponen penting:
1. Import MCP SDK
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
McpServer: Class utama untuk membuat serverStdioServerTransport: Transport layer menggunakan stdio (standard untuk local MCP)
2. Define Schema dengan Zod
const GetWeatherSchema = z.object({
city: z.string().describe("Nama kota untuk mendapatkan data cuaca"),
});
Schema ini mendefinisikan input yang diterima tool. AI akan menggunakan description untuk memahami apa yang harus diinput.
3. Register Tool
server.tool(
"get_weather", // nama tool
"Mendapatkan informasi cuaca...", // deskripsi
GetWeatherSchema.shape, // input schema
async ({ city }) => { ... } // handler function
);
Setiap tool punya nama, deskripsi, schema, dan handler. Handler adalah async function yang akan dijalankan saat AI memanggil tool.
4. Return Format
return {
content: [
{
type: "text",
text: JSON.stringify(data),
},
],
};
MCP menggunakan format content array. Untuk text response, gunakan type: "text".
2.3 Build dan Test Manual
# Build TypeScript
npm run build
# Test jalankan server (akan hang karena menunggu input)
npm run start
Jika tidak ada error, server sudah siap. Tapi untuk test yang lebih proper, kita perlu MCP Inspector.
Step 3: Testing dengan MCP Inspector
MCP Inspector adalah tool official untuk debugging dan testing MCP servers. Ini seperti Postman-nya MCP.
3.1 Install dan Jalankan MCP Inspector
# Jalankan MCP Inspector dengan npx
npx @modelcontextprotocol/inspector
Inspector akan start di http://localhost:5173.
3.2 Connect ke Server
- Buka browser ke
http://localhost:5173 - Di field “Command”, masukkan path ke server:
node - Di field “Arguments”, masukkan:
/path/to/mcp-weather-server/dist/index.js - Klik “Connect”
3.3 Test Tool
Setelah connected:
- Klik tab “Tools”
- Kamu akan melihat
get_weathertool - Klik tool tersebut
- Masukkan input:
{"city": "Jakarta"} - Klik “Run”
Expected output:
{
"success": true,
"data": {
"city": "Jakarta",
"temperature": "32°C",
"condition": "Partly Cloudy",
"humidity": "75%",
"wind_speed": "12 km/h"
}
}
Jika output sesuai, selamat! MCP Server kamu sudah berfungsi dengan baik.
Step 4: Integrasi dengan Claude Desktop
Sekarang mari kita hubungkan MCP Server dengan Claude Desktop agar Claude bisa langsung menggunakan weather tool kita.
4.1 Lokasi Config File
Claude Desktop menyimpan konfigurasi MCP di:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
4.2 Buat atau Edit Config
# macOS - buat directory jika belum ada
mkdir -p ~/Library/Application\ Support/Claude
# Buka/buat config file
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
4.3 Tambahkan Server Config
Paste konfigurasi berikut (sesuaikan path dengan lokasi project-mu):
{
"mcpServers": {
"weather": {
"command": "node",
"args": [
"/Users/username/mcp-weather-server/dist/index.js"
]
}
}
}
Penting: Ganti /Users/username/mcp-weather-server dengan absolute path ke project-mu.
4.4 Restart Claude Desktop
- Quit Claude Desktop completely (Cmd+Q di macOS)
- Buka kembali Claude Desktop
- Cari icon 🔌 atau tools icon di interface
4.5 Test di Claude
Coba tanyakan ke Claude:
“Bagaimana cuaca di Jakarta hari ini?”
Claude akan:
- Mengenali bahwa pertanyaan ini bisa dijawab dengan
get_weathertool - Memanggil tool dengan parameter
{"city": "Jakarta"} - Menggunakan response untuk menjawab pertanyaanmu
Contoh response Claude:
“Berdasarkan data yang saya dapatkan, cuaca di Jakarta saat ini:
- Suhu: 32°C
- Kondisi: Partly Cloudy
- Kelembaban: 75%
- Kecepatan angin: 12 km/h
Cuaca cukup panas dan lembab, jadi pastikan untuk tetap terhidrasi jika keluar rumah!”
Step 5: Integrasi dengan Cursor IDE
Cursor IDE juga mendukung MCP, memungkinkan AI coding assistant-nya menggunakan custom tools.
5.1 Lokasi Cursor Config
Cursor menyimpan MCP config di:
- macOS:
~/.cursor/mcp.json - Windows:
%USERPROFILE%\.cursor\mcp.json - Linux:
~/.cursor/mcp.json
5.2 Buat Config File
# Buat directory jika belum ada
mkdir -p ~/.cursor
# Buat config file
touch ~/.cursor/mcp.json
5.3 Tambahkan Server Config
{
"mcpServers": {
"weather": {
"command": "node",
"args": [
"/Users/username/mcp-weather-server/dist/index.js"
]
}
}
}
5.4 Restart Cursor
- Restart Cursor IDE
- Buka Cursor Settings (Cmd+,)
- Cari “MCP” untuk verify server terdaftar
5.5 Test di Cursor
Di Cursor chat, coba:
“Tolong cek cuaca di Bandung”
Cursor AI akan menggunakan MCP tool untuk fetch data cuaca.
Advanced: Multiple Tools dalam Satu Server
Satu MCP Server bisa punya banyak tools. Mari kita extend weather server dengan tools tambahan.
Menambahkan Tool get_forecast
Update src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Schemas
const GetWeatherSchema = z.object({
city: z.string().describe("Nama kota untuk mendapatkan data cuaca"),
});
const GetForecastSchema = z.object({
city: z.string().describe("Nama kota untuk forecast"),
days: z.number().min(1).max(7).describe("Jumlah hari forecast (1-7)"),
});
const CompareWeatherSchema = z.object({
cities: z.array(z.string()).min(2).max(5).describe("Array nama kota untuk dibandingkan (2-5 kota)"),
});
// Types
interface WeatherData {
city: string;
temperature: number;
condition: string;
humidity: number;
wind_speed: number;
}
interface ForecastDay {
date: string;
high: number;
low: number;
condition: string;
}
// Helper functions
async function fetchWeather(city: string): Promise<WeatherData> {
// Mock implementation - sama seperti sebelumnya
const mockWeatherData: Record<string, WeatherData> = {
"jakarta": {
city: "Jakarta",
temperature: 32,
condition: "Partly Cloudy",
humidity: 75,
wind_speed: 12,
},
"bandung": {
city: "Bandung",
temperature: 24,
condition: "Sunny",
humidity: 60,
wind_speed: 8,
},
"surabaya": {
city: "Surabaya",
temperature: 34,
condition: "Hot",
humidity: 70,
wind_speed: 15,
},
"bali": {
city: "Bali",
temperature: 30,
condition: "Tropical",
humidity: 80,
wind_speed: 10,
},
};
const cityLower = city.toLowerCase();
if (mockWeatherData[cityLower]) {
return mockWeatherData[cityLower];
}
return {
city: city,
temperature: Math.floor(Math.random() * 15) + 20,
condition: "Clear",
humidity: Math.floor(Math.random() * 30) + 50,
wind_speed: Math.floor(Math.random() * 20) + 5,
};
}
async function fetchForecast(city: string, days: number): Promise<ForecastDay[]> {
const conditions = ["Sunny", "Cloudy", "Rainy", "Partly Cloudy", "Thunderstorm"];
const forecast: ForecastDay[] = [];
const today = new Date();
for (let i = 0; i < days; i++) {
const date = new Date(today);
date.setDate(date.getDate() + i);
forecast.push({
date: date.toISOString().split('T')[0],
high: Math.floor(Math.random() * 10) + 28,
low: Math.floor(Math.random() * 5) + 22,
condition: conditions[Math.floor(Math.random() * conditions.length)],
});
}
return forecast;
}
// Initialize server
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
});
// Tool 1: get_weather (existing)
server.tool(
"get_weather",
"Mendapatkan informasi cuaca terkini untuk sebuah kota",
GetWeatherSchema.shape,
async ({ city }) => {
try {
const weather = await fetchWeather(city);
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
data: {
city: weather.city,
temperature: `${weather.temperature}°C`,
condition: weather.condition,
humidity: `${weather.humidity}%`,
wind_speed: `${weather.wind_speed} km/h`,
},
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: `Gagal mendapatkan data cuaca: ${error}`,
}),
},
],
isError: true,
};
}
}
);
// Tool 2: get_forecast (NEW)
server.tool(
"get_forecast",
"Mendapatkan prakiraan cuaca untuk beberapa hari ke depan",
GetForecastSchema.shape,
async ({ city, days }) => {
try {
const forecast = await fetchForecast(city, days);
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
city: city,
forecast: forecast.map(day => ({
date: day.date,
high: `${day.high}°C`,
low: `${day.low}°C`,
condition: day.condition,
})),
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: `Gagal mendapatkan forecast: ${error}`,
}),
},
],
isError: true,
};
}
}
);
// Tool 3: compare_weather (NEW)
server.tool(
"compare_weather",
"Membandingkan cuaca antara beberapa kota sekaligus",
CompareWeatherSchema.shape,
async ({ cities }) => {
try {
const weatherPromises = cities.map(city => fetchWeather(city));
const weatherResults = await Promise.all(weatherPromises);
const comparison = weatherResults.map(w => ({
city: w.city,
temperature: `${w.temperature}°C`,
condition: w.condition,
humidity: `${w.humidity}%`,
}));
// Sort by temperature (hottest first)
comparison.sort((a, b) => {
const tempA = parseInt(a.temperature);
const tempB = parseInt(b.temperature);
return tempB - tempA;
});
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
comparison: comparison,
hottest: comparison[0].city,
coolest: comparison[comparison.length - 1].city,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: `Gagal membandingkan cuaca: ${error}`,
}),
},
],
isError: true,
};
}
}
);
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP Server running with 3 tools");
}
main().catch(console.error);
Rebuild dan Test
npm run build
Sekarang server punya 3 tools:
get_weather: Cuaca saat iniget_forecast: Prakiraan beberapa haricompare_weather: Bandingkan cuaca antar kota
Contoh penggunaan di Claude:
“Bandingkan cuaca Jakarta, Bandung, dan Surabaya”
Troubleshooting Common Errors
Error 1: “Cannot find module”
Error: Cannot find module '@modelcontextprotocol/sdk/server/mcp.js'
Solusi: Pastikan install dengan npm, bukan yarn. Dan pastikan "type": "module" ada di package.json.
rm -rf node_modules package-lock.json
npm install
Error 2: “Server not showing in Claude”
Checklist:
- Path di config harus absolute, bukan relative
- File harus sudah di-build (
npm run build) - Claude Desktop sudah di-restart sepenuhnya (Cmd+Q, bukan close window)
Debug: Cek log Claude Desktop:
# macOS
tail -f ~/Library/Logs/Claude/mcp*.log
Error 3: “Tool execution failed”
Solusi: Pastikan handler function return format yang benar:
return {
content: [
{
type: "text",
text: "your response here",
},
],
};
Error 4: “ENOENT” atau “spawn error”
Solusi: Path ke node atau file tidak ditemukan. Gunakan absolute path:
{
"mcpServers": {
"weather": {
"command": "/usr/local/bin/node",
"args": ["/full/path/to/dist/index.js"]
}
}
}
Cek path node:
which node
# Output: /usr/local/bin/node
Error 5: TypeScript compilation errors
Solusi: Pastikan tsconfig.json sesuai dengan yang di tutorial. Key settings:
"module": "Node16""moduleResolution": "Node16"
Tips dan Best Practices
1. Gunakan Descriptive Names
// ✅ Good
server.tool(
"get_weather_by_city",
"Mendapatkan data cuaca berdasarkan nama kota"
);
// ❌ Bad
server.tool("gw", "weather");
2. Validate Input Thoroughly
const schema = z.object({
city: z.string()
.min(2, "Nama kota minimal 2 karakter")
.max(100, "Nama kota maksimal 100 karakter"),
days: z.number()
.int("Harus bilangan bulat")
.min(1, "Minimal 1 hari")
.max(7, "Maksimal 7 hari"),
});
3. Handle Errors Gracefully
try {
const result = await riskyOperation();
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true,
};
}
4. Log for Debugging
// Gunakan console.error karena stdout dipakai untuk MCP communication
console.error(`Processing request for city: ${city}`);
5. Environment Variables untuk Secrets
const API_KEY = process.env.WEATHER_API_KEY;
if (!API_KEY) {
throw new Error("WEATHER_API_KEY environment variable required");
}
Resources dan Next Steps
Official Documentation
Ideas untuk Tools Selanjutnya
- Database Tool: Query PostgreSQL/MySQL langsung dari Claude
- File Manager: Create, read, update files di directory tertentu
- Git Tool: Check status, create commits, push changes
- API Client: Hit internal company APIs
- Slack/Discord Bot: Send messages, read channels
Community
Kesimpulan
Kamu sekarang sudah bisa:
- ✅ Setup MCP Server project dari nol
- ✅ Membuat custom tools dengan TypeScript
- ✅ Testing dengan MCP Inspector
- ✅ Integrasi dengan Claude Desktop
- ✅ Integrasi dengan Cursor IDE
- ✅ Handle multiple tools dalam satu server
MCP membuka kemungkinan baru untuk customisasi AI assistant. Bayangkan Claude yang bisa langsung akses database production-mu, atau Cursor yang bisa deploy ke server dengan satu command.
Mulai dari tool sederhana seperti weather, lalu gradually build tools yang lebih kompleks sesuai kebutuhan workflow-mu. Happy coding! 🚀
Punya pertanyaan atau stuck di salah satu step? Drop comment di bawah atau reach out via Twitter. Saya akan bantu troubleshoot!