Menghubungkan coding agent (seperti Cursor atau Claude Code) langsung ke web sering menghasilkan output berulang atau token exhaustion akibat payload HTML dan komentar diskusi yang tidak relevan. Menggunakan Model Context Protocol (MCP) server lokal memungkinkan agen melakukan query ke Stack Overflow API secara terstruktur, memfilter hanya solusi valid (accepted answers), dan membatasi konsumsi context window.

Arsitektur MCP Server Stack Overflow

MCP server bertindak sebagai jembatan stdin/stdout (stdio) antara client agent dan Stack Exchange API. Arsitektur runtime ini terdiri dari tiga komponen utama:

  • Stdio Transport: Channel komunikasi low-latency antara client host agent dan proses Node.js lokal.
  • Local Cache Layer (SQLite): Menyimpan payload response API berdasarkan hash query untuk menghindari throttling IP. Stack Exchange API membatasi 300 request per hari tanpa client key, atau 10.000 dengan key.
  • Schema Filtering & Truncation: Membuang metadata non-esensial (seperti avatar, reputasi user, vote breakdown) dan memotong konten panjang sebelum dikirim ke context window agent.

Skema Filtering Response

Agen hanya membutuhkan inti pertanyaan dan jawaban terverifikasi. Filter parameter Stack Exchange API (!nNPvSNdWjv atau kustom) dikombinasikan dengan pembersihan lokal menggunakan sanitizer teks sederhana untuk mengubah HTML jawaban menjadi Markdown murni.

Implementasi Server dengan TypeScript

Inisialisasi project dan install dependency inti:

npm init -y
npm install @modelcontextprotocol/sdk better-sqlite3 dotenv
npm install --save-dev typescript @types/node @types/better-sqlite3

Berikut implementasi server minimal pada src/index.ts:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import Database from "better-sqlite3";

const db = new Database("so_cache.db");
db.exec(`CREATE TABLE IF NOT EXISTS cache (query TEXT PRIMARY KEY, response TEXT, timestamp INTEGER)`);

const server = new Server(
  { name: "stackoverflow-context-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "search_solution",
    description: "Cari solusi terverifikasi dari Stack Overflow berdasarkan error atau masalah spesifik.",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Query pencarian teknis atau pesan error" },
        tagged: { type: "string", description: "Tag bahasa/framework, misal: typescript, react" }
      },
      required: ["query"]
    }
  }]
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name !== "search_solution") {
    throw new Error("Tool tidak ditemukan");
  }

  const { query, tagged } = request.params.arguments as { query: string; tagged?: string };
  const cacheKey = `${query}_${tagged || ""}`;
  
  const cached = db.prepare("SELECT response FROM cache WHERE query = ? AND (? - timestamp) < 86400000").get(cacheKey, Date.now()) as { response: string } | undefined;
  if (cached) {
    return { content: [{ type: "text", text: cached.response }] };
  }

  const url = new URL("https://api.stackexchange.com/2.2/search/advanced");
  url.searchParams.set("order", "desc");
  url.searchParams.set("sort", "relevance");
  url.searchParams.set("q", query);
  url.searchParams.set("accepted", "True");
  url.searchParams.set("site", "stackoverflow");
  url.searchParams.set("filter", "withbody");
  if (tagged) url.searchParams.set("tagged", tagged);

  const res = await fetch(url.toString());
  const data = await res.json();

  if (!data.items || data.items.length === 0) {
    return { content: [{ type: "text", text: "Tidak ditemukan solusi terverifikasi untuk query ini." }] };
  }

  // Batasi hanya mengambil 2 hasil paling relevan untuk menjaga context window
  const results = data.items.slice(0, 2).map((item: any) => ({
    title: item.title,
    link: item.link,
    body_snippet: item.body.replace(/<[^>]*>?/gm, "").slice(0, 1500)
  }));

  const outputText = JSON.stringify(results, null, 2);
  db.prepare("INSERT OR REPLACE INTO cache (query, response, timestamp) VALUES (?, ?, ?)").run(cacheKey, outputText, Date.now());

  return { content: [{ type: "text", text: outputText }] };
});

const transport = new StdioServerTransport();
await server.connect(transport);

Konfigurasi Client Host

Tambahkan konfigurasi MCP server pada client environment Anda. Untuk Claude Desktop, sesuaikan claude_desktop_config.json:

{
  "mcpServers": {
    "stackoverflow": {
      "command": "node",
      "args": ["/path/to/so-mcp-server/dist/index.js"]
    }
  }
}

Untuk Cursor, konfigurasi tool dapat ditambahkan melalui panel Settings > Features > MCP Servers dengan memilih transport command.

Optimasi Context Window dan Debugging

Integrasi retrieval agent rentan terhadap masalah konsumsi resource jika tidak dibatasi dengan ketat:

  • Token Exhaustion: Stack Overflow answers sering mengandung dump stack trace yang panjang. Kode di atas membatasi panjang cuplikan jawaban maksimal 1500 karakter per entry (slice(0, 1500)). Jangan mengembalikan raw HTML utuh ke model.
  • Rate Limiting (HTTP 400/429): Jika response Stack Exchange mengembalikan backoff field, server wajib menghormati durasi backoff tersebut sebelum mengirimkan request berikutnya. TTL cache 24 jam via SQLite secara efektif menekan pemanggilan endpoint berulang.
  • Error Stdio: Pastikan script Node.js tidak memanggil console.log() untuk debugging karena akan merusak framing JSON-RPC via stdio. Gunakan console.error() untuk logging debug tanpa mengganggu protocol stream.