Arsitektur Remote Agent ke Local Repo

Menghubungkan remote AI agent (seperti OpenAI ChatGPT Developer Mode atau Anthropic custom client) langsung ke mesin lokal tanpa proteksi membuka risiko intrusi jaringan dan pembacaan arbitrary files. Model Context Protocol (MCP) menstandarkan pertukaran context melalui JSON-RPC, namun membutuhkan transport layer yang aman saat agent berada di luar local network.

Pola arsitektur bridge menggunakan Cloudflare Tunnel mengeliminasi kebutuhan port-forwarding router dan IP publik statis. Alur komunikasi data berjalan secara terenkripsi:

[Remote AI Agent] 
  │ (HTTPS + Cloudflare Service Token Header)
  ▼
[Cloudflare Edge / Cloudflare Access] (Validasi Token Service Auth)
  │ (Tunnel Terenkripsi / QUIC/HTTP2)
  ▼
[cloudflared daemon] (Berjalan di host lokal)
  │ (Reverse Proxy ke loopback)
  ▼
[Local MCP Server : 8080] (Validasi Path Boundary + Sanitasi CLI)
  │
  ▼
[Target Git Repository / Local Filesystem]

Implementasi Minimal Server MCP (Node.js)

Implementasi ini menggunakan transport Server-Sent Events (SSE) via @modelcontextprotocol/sdk dan express. Server mengekspos dua tools: inspeksi file lokal dan inspeksi status Git.

// package.json dependencies: @modelcontextprotocol/sdk, express, zod
import express from 'express';
import path from 'node:path';
import fs from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { z } from 'zod';

const execFileAsync = promisify(execFile);
const REPO_ROOT = path.resolve(process.env.TARGET_REPO || process.cwd());

// 1. Guard Path Traversal Boundary
function resolveSafePath(userPath) {
  const resolved = path.resolve(REPO_ROOT, userPath);
  const rel = path.relative(REPO_ROOT, resolved);
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
    throw new Error('Akses ditolak: Jalur berada di luar direktori repositori.');
  }
  return resolved;
}

const server = new McpServer({
  name: 'local-repo-bridge',
  version: '1.0.0'
});

// Tool: Baca isi file
server.tool(
  'read_file',
  'Membaca konten file dalam batas repository',
  { relative_path: z.string() },
  async ({ relative_path }) => {
    try {
      const safePath = resolveSafePath(relative_path);
      const content = await fs.readFile(safePath, 'utf-8');
      return { content: [{ type: 'text', text: content }] };
    } catch (err) {
      return { isError: true, content: [{ type: 'text', text: err.message }] };
    }
  }
);

// Tool: Git Diff
server.tool(
  'git_diff',
  'Mengambil git diff repositori target',
  { staged: z.boolean().default(false) },
  async ({ staged }) => {
    try {
      const args = ['diff'];
      if (staged) args.push('--staged');
      
      // Eksekusi git menggunakan execFile, bukan exec shell
      const { stdout } = await execFileAsync('git', args, { 
        cwd: REPO_ROOT,
        maxBuffer: 10 * 1024 * 1024
      });
      return { content: [{ type: 'text', text: stdout || 'Tidak ada perubahan.' }] };
    } catch (err) {
      return { isError: true, content: [{ type: 'text', text: err.message }] };
    }
  }
);

const app = express();
let transport = null;

app.get('/sse', async (req, res) => {
  transport = new SSEServerTransport('/message', res);
  await server.connect(transport);
});

app.post('/message', async (req, res) => {
  if (transport) {
    await transport.handlePostMessage(req, res);
  } else {
    res.status(400).send('Sesi SSE belum aktif');
  }
});

const PORT = process.env.PORT || 8080;
app.listen(PORT, '127.0.0.1', () => {
  console.log(`MCP Server berjalan di 127.0.0.1:${PORT} untuk repo: ${REPO_ROOT}`);
});

Konfigurasi Cloudflare Tunnel dengan Service Auth

Membuka port server lokal ke internet publik melalui Cloudflare Tunnel tanpa autentikasi akan mengekspos endpoint tool execution ke bot crawling. Gunakan Cloudflare Access dengan aturan Service Token.

1. Inisialisasi Tunnel

Jalankan CLI cloudflared untuk mendaftarkan tunnel baru:

cloudflared tunnel create mcp-repo-tunnel
cloudflared tunnel route dns mcp-repo-tunnel mcp.internal-domain.com

2. Konfigurasi config.yml

Buat file konfigurasi tunnel di ~/.cloudflared/config.yml:

tunnel: <TUNNEL_UUID>
credentials-file: /home/user/.cloudflared/<TUNNEL_UUID>.json

ingress:
  - hostname: mcp.internal-domain.com
    service: http://127.0.0.1:8080
    originRequest:
      noTLSVerify: false
  - service: http_status:404

Jalankan daemon tunnel:

cloudflared tunnel run mcp-repo-tunnel

3. Penerapan Cloudflare Access Rule

  1. Buka Cloudflare Zero Trust Dashboard > Access > Service Auth > Create Service Token. Catat Client ID dan Client Secret.
  2. Navigasi ke Applications > Add an Application > Pilih Self-hosted.
  3. Masukkan domain aplikasi: mcp.internal-domain.com.
  4. Buat Access Policy dengan Action: Service Auth dan Rule Type: Service Token. Pilih nama token yang dibuat di langkah 1.

Setiap request tanpa header CF-Access-Client-Id dan CF-Access-Client-Secret akan diblokir dengan status HTTP 403 di edge Cloudflare sebelum menyentuh tunnel lokal.

Mitigasi Keamanan Wajib

1. Path Traversal Guard

Penggunaan raw path dari LLM rawan serangan ../ untuk membaca /etc/passwd atau ~/.ssh/id_rsa. Validasi canonical path mutlak dilakukan:

  • Gunakan path.resolve(REPO_ROOT, input) untuk mendapatkan absolut path.
  • Evaluasi jalur relatif dengan path.relative(REPO_ROOT, resolvedPath). Jika string hasil evaluasi diawali .. atau berbentuk absolute root, tolak request.
  • Gunakan fs.realpath jika ada kebutuhan mendeteksi symbolic link yang keluar dari root directory.

2. Sanitasi Command Execution

Jangan pernah memanggil child_process.exec menggunakan input arbitrary karena parsing shell string membuka celah code injection (misal: ; rm -rf /). Gunakan selalu child_process.execFile dengan array argumen statis:

// Salah (Shell execution): 
// exec(`git diff ${userInput}`) -> Rentan injection

// Benar (Direct binary execution):
execFile('git', ['diff', '--', targetFile], { cwd: REPO_ROOT });

3. Boundary Allowlist & Denylist

Blokir pembacaan direktori dan file sensitif secara eksplisit di dalam server MCP:

  • Denylist wajib: .git/, .env, *.pem, *.key, node_modules/.
  • Bila memungkinkan, gunakan read-only filesystem mode untuk memastikan agent tidak dapat memodifikasi file tanpa konfirmasi eksplisit dari operator.

Checklist Verifikasi Koneksi End-to-End

  1. Verifikasi Blokir Unauthorized (Edge Test):
    curl -I https://mcp.internal-domain.com/sse

    Respons harus menghasilkan status HTTP/2 403 Forbidden.

  2. Verifikasi Autentikasi Service Token:
    curl -N -H "CF-Access-Client-Id: <CLIENT_ID>" \
         -H "CF-Access-Client-Secret: <CLIENT_SECRET>" \
         https://mcp.internal-domain.com/sse

    Respons harus mengembalikan header content-type: text/event-stream dengan endpoint event /message.

  3. Verifikasi Path Guard (Traversal Test):
    # Kirim JSON-RPC tool call read_file untuk ../../../etc/passwd
    # Server wajib mengembalikan flag isError: true
  4. Verifikasi Remote Agent Integration:

    Konfigurasikan remote client MCP dengan URL https://mcp.internal-domain.com/sse dan daftarkan kedua custom header autentikasi Cloudflare Access. Lakukan instruksi natural language: "Cek git diff terakhir dari repositori" dan pastikan tool git_diff tereksekusi di mesin lokal.