MCP Server: Cara Membuat AI Tools Custom untuk Claude dan Cursor - Nayaka Yoga Pradipta

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?

  1. Custom Tools: Buat tools sesuai kebutuhan spesifik — akses database, call internal API, manipulasi file, dll.
  2. Standardized Protocol: Satu server bisa digunakan di berbagai AI clients (Claude, Cursor, VSCode, dll.)
  3. Local First: Data sensitif tetap di local machine, tidak perlu kirim ke cloud.
  4. 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 Server
  • zod: Library untuk schema validation (define input/output tools)
  • typescript: TypeScript compiler
  • tsx: 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 server
  • StdioServerTransport: 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

  1. Buka browser ke http://localhost:5173
  2. Di field “Command”, masukkan path ke server:
    node
  3. Di field “Arguments”, masukkan:
    /path/to/mcp-weather-server/dist/index.js
  4. Klik “Connect”

3.3 Test Tool

Setelah connected:

  1. Klik tab “Tools”
  2. Kamu akan melihat get_weather tool
  3. Klik tool tersebut
  4. Masukkan input: {"city": "Jakarta"}
  5. 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

  1. Quit Claude Desktop completely (Cmd+Q di macOS)
  2. Buka kembali Claude Desktop
  3. Cari icon 🔌 atau tools icon di interface

4.5 Test di Claude

Coba tanyakan ke Claude:

“Bagaimana cuaca di Jakarta hari ini?”

Claude akan:

  1. Mengenali bahwa pertanyaan ini bisa dijawab dengan get_weather tool
  2. Memanggil tool dengan parameter {"city": "Jakarta"}
  3. 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

  1. Restart Cursor IDE
  2. Buka Cursor Settings (Cmd+,)
  3. 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 ini
  • get_forecast: Prakiraan beberapa hari
  • compare_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:

  1. Path di config harus absolute, bukan relative
  2. File harus sudah di-build (npm run build)
  3. 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

  1. Database Tool: Query PostgreSQL/MySQL langsung dari Claude
  2. File Manager: Create, read, update files di directory tertentu
  3. Git Tool: Check status, create commits, push changes
  4. API Client: Hit internal company APIs
  5. Slack/Discord Bot: Send messages, read channels

Community

Kesimpulan

Kamu sekarang sudah bisa:

  1. ✅ Setup MCP Server project dari nol
  2. ✅ Membuat custom tools dengan TypeScript
  3. ✅ Testing dengan MCP Inspector
  4. ✅ Integrasi dengan Claude Desktop
  5. ✅ Integrasi dengan Cursor IDE
  6. ✅ 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!