Langsung ke konten
Integration

OpenAPI ke server MCP: 150 titik akhir, 49 alat AI

| 9 min read

Bagaimana kami mengonversi spesifikasi OpenAPI menjadi server MCP yang dikurasi dengan 49 alat. Konversi skema, deskripsi alat, anotasi, dan transportasi HTTP tanpa kewarganegaraan.

Laptop showing code in a dark development environment
Photo by Clement Helardot on Unsplash

REST API Botoi memiliki 150+ titik akhir. Saat kami membangun server MCP, kami mendaftarkan 49 di antaranya sebagai alat. Bukan karena sisanya tidak berfungsi. Karena memberikan 150 alat pada model AI seperti memberikan menu setebal 200 halaman kepada seseorang; mereka akan memilih sesuatu, tapi itu bukan hal yang benar.

Postingan ini menjelaskan proses lengkapnya: menyusun daftar alat, mengonversi skema OpenAPI menjadi objek Zod, menulis deskripsi model AI yang diurai dengan baik, menambahkan anotasi MCP, dan menjalankan semuanya sebagai Cloudflare Worker tanpa kewarganegaraan. Jika Anda memelihara API dan ingin membangun server MCP darinya, inilah pedomannya.

Mengapa 49 alat, bukan 150

Setiap alat yang Anda daftarkan di server MCP akan diserialkan ke dalam jendela konteks model. Nama alat, deskripsi, dan skema masukan lengkap semuanya menggunakan token. Manifes berisi 150 alat dapat membakar 30.000+ token sebelum pengguna mengetik satu kata pun.

Hal ini menimbulkan dua masalah:

  • Lebih sedikit token yang tersisa untuk percakapan itu sendiri
  • Model lebih sering memilih alat yang salah jika daftarnya panjang

Kami menguji ini. Dengan 150+ titik akhir terdaftar, Claude memilih alat yang tepat pada percobaan pertama sekitar 72% dari keseluruhan waktu. Dengan 49 alat yang dikurasi, jumlah tersebut melonjak menjadi 94%. Daftar yang lebih kecil dan terfokus menjadikan model lebih baik dalam tugasnya.

Kriteria kurasinya sederhana:

  • Apakah agen AI memerlukan percakapan di tengah-tengah ini? (Pencarian DNS: ya. Pembuatan PDF: jarang.)
  • Apakah alat tersebut mengembalikan data terstruktur yang dapat dijadikan alasan oleh model? (JSON: ya. Gambar biner: tidak.)
  • Bisakah model mengisi parameter yang diperlukan dari bahasa alami? (Nama domain: ya. Objek konfigurasi bersarang yang kompleks: tidak.)

Struktur manifes alat

Setiap alat yang dikurasi memetakan nama alat MCP ke jalur API, metode HTTP, deskripsi, dan anotasi. Inilah antarmuka TypeScript:

export interface CuratedTool {
  path: string;
  method: 'get' | 'post';
  title: string;
  description: string;
  annotations: {
    readOnlyHint?: boolean;
    destructiveHint?: boolean;
    idempotentHint?: boolean;
    openWorldHint?: boolean;
  };
}

Dan inilah dua entri dalam praktiknya:

// curated-tools.ts
export const CURATED_TOOLS: Record<string, CuratedTool> = {
  lookup_dns: {
    path: '/v1/dns/lookup',
    method: 'post',
    title: 'DNS Lookup',
    description:
      'Query DNS records (A, AAAA, MX, TXT, CNAME, NS) for a domain. ' +
      'Use when you need to check DNS configuration or troubleshoot domain resolution.',
    annotations: { readOnlyHint: true, openWorldHint: true },
  },
  dev_hash: {
    path: '/v1/hash',
    method: 'post',
    title: 'Hash Text',
    description:
      'Generate a hash (MD5, SHA-1, SHA-256, SHA-512) of input text. ' +
      'Use for checksums, data integrity, or fingerprinting.',
    annotations: { readOnlyHint: true },
  },
  // ... 47 more tools
};

Itu path Dan method bidang menunjuk ke titik akhir REST yang ada. Itu description memberi tahu model kapan harus menggunakan alat tersebut. Itu annotations beri tahu model bagaimana alat tersebut berperilaku.

Mengonversi skema OpenAPI ke Zod

MCP SDK mengharapkan skema input alat sebagai objek Zod. API kami sudah memiliki spesifikasi OpenAPI 3.1 dengan definisi isi permintaan lengkap untuk setiap titik akhir. Pembuat skema membaca definisi tersebut dan menghasilkan tipe Zod saat startup server.

Fungsi konversi inti memetakan setiap tipe properti OpenAPI ke setara Zod-nya:

// schema-builder.ts
import { z } from 'zod';
import { paths } from '../../openapi-paths';

function mapPropertyToZod(
  prop: OpenApiProperty,
  isRequired: boolean
): z.ZodTypeAny {
  let schema: z.ZodTypeAny;

  if (prop.enum && prop.enum.length > 0) {
    schema = z.enum(prop.enum as [string, ...string[]]);
  } else {
    switch (prop.type) {
      case 'number':
      case 'integer':
        schema = z.number();
        break;
      case 'boolean':
        schema = z.boolean();
        break;
      case 'array':
        schema = z.array(z.string());
        break;
      case 'object':
        schema = z.record(z.unknown());
        break;
      default:
        schema = z.string();
        break;
    }
  }

  if (prop.description) {
    schema = schema.describe(prop.description);
  }
  if (prop.default !== undefined) {
    schema = schema.default(prop.default);
  }
  if (!isRequired) {
    schema = schema.optional();
  }

  return schema;
}

Keputusan penting dalam fungsi ini:

  • enum nilai-nilai menjadi z.enum(), memberikan model serangkaian opsi valid yang tetap
  • Bidang yang wajib diisi tetap wajib diisi; bidang opsional dapatkan .optional()
  • OpenAPI description terbawa melalui .describe(), yang disertakan oleh MCP SDK dalam manifes alat
  • Nilai default menyebar melalui .default()

Itu buildZodSchema fungsi menangani titik akhir POST (isi permintaan) dan GET (parameter kueri):

export function buildZodSchema(
  apiPath: string,
  method: 'get' | 'post'
): Record<string, z.ZodTypeAny> {
  const operation = getOperation(apiPath, method);
  if (!operation) return {};

  // POST: read from requestBody schema
  if (method === 'post') {
    const schema = operation.requestBody
      ?.content?.['application/json']?.schema;
    if (!schema?.properties) return {};

    const required = new Set(schema.required ?? []);
    const result: Record<string, z.ZodTypeAny> = {};

    for (const [key, prop] of Object.entries(schema.properties)) {
      result[key] = mapPropertyToZod(prop, required.has(key));
    }
    return result;
  }

  // GET: read from query parameters
  const params = operation.parameters;
  if (!params || params.length === 0) return {};

  const result: Record<string, z.ZodTypeAny> = {};
  for (const param of params) {
    if (param.in !== 'query') continue;
    const prop: OpenApiProperty = {
      type: param.schema?.type ?? 'string',
      description: param.description ?? param.schema?.description,
      default: param.schema?.default,
      enum: param.schema?.enum,
    };
    result[param.name] = mapPropertyToZod(prop, param.required === true);
  }
  return result;
}

Fungsi ini berjalan satu kali per alat selama pembuatan server. Ini mengembalikan flat Record<string, z.ZodTypeAny> bahwa MCP SDK diserialisasikan ke dalam Skema JSON untuk manifes alat.

Deskripsi alat tulis model AI diurai dengan baik

Deskripsi alat adalah bidang terpenting untuk pemilihan alat yang benar. Model membacanya untuk memutuskan apakah suatu alat sesuai dengan maksud pengguna. Deskripsi yang tidak jelas menyebabkan pemilihan alat yang salah.

Kami memilih pola dua kalimat:

  1. Kalimat pertama: apa yang dilakukan alat tersebut, dimulai dengan kata kerja. Sertakan tipe atau format data spesifik yang ditanganinya.
  2. Kalimat kedua: kapan menggunakannya, dimulai dengan "Gunakan kapan". Hal ini memberikan model kondisi pemicu.

Bandingkan dua deskripsi berikut untuk alat pencarian DNS yang sama:

Versi Keterangan Masalah
Buruk "Alat DNS untuk mencari segala sesuatunya" Tidak ada jenis catatan yang terdaftar, tidak ada kondisi pemicu, tidak jelas
Bagus "Kueri data DNS (A, AAAA, MX, TXT, CNAME, NS) untuk domain. Gunakan saat Anda perlu memeriksa konfigurasi DNS atau memecahkan masalah resolusi domain." Tidak ada

Versi yang baik memberi tahu model jenis data persis yang dapat dikueri (sehingga model mengetahui bahwa alat ini menangani pencarian MX) dan situasi yang seharusnya memicunya (pemeriksaan konfigurasi DNS, pemecahan masalah). Model ini mencocokkan niat pengguna dengan kata kunci tersebut.

Anotasi MCP: memberi tahu model bagaimana alat berperilaku

Anotasi adalah tanda metadata pada setiap alat. Mereka tidak mempengaruhi eksekusi. Mereka memberi tahu model tentang jenis efek samping yang diharapkan.

// Read-only tool that hits an external service
lookup_dns: {
  annotations: { readOnlyHint: true, openWorldHint: true },
}

// Encryption tool: no external calls, same input = same output
security_encrypt: {
  annotations: { idempotentHint: true },
}

Empat anotasi dan isyaratnya:

Anotasi Sinyal Contoh
readOnlyHint Alat ini membaca data tetapi tidak pernah mengubah apa pun Pencarian DNS, WHOIS, pemeriksaan SSL
destructiveHint Alat ini menghapus atau menimpa data Penghapusan kotak masuk webhook (tidak ada dalam kumpulan pilihan kami)
idempotentHint Memanggil alat ini dua kali dengan masukan yang sama menghasilkan hasil yang sama Enkripsi AES, dekripsi AES
openWorldHint Alat ini membuat permintaan jaringan eksternal Pencarian IP, metadata URL, deteksi teknologi

Dari 49 alat kami, 44 dibawa readOnlyHint: true. 12 alat pencarian juga dibawa openWorldHint: true karena mereka memanggil server DNS eksternal, registrasi WHOIS, atau mengambil halaman web aktif. Alat enkripsi/dekripsi dibawa idempotentHint: true karena itu adalah transformasi deterministik.

Tidak ada alat pilihan kami yang bisa dibawa destructiveHint. Itu adalah pilihan yang disengaja. Kami mengecualikan alat seperti penghapusan kotak masuk webhook dan penghapusan tempel dari kumpulan hasil kurasi karena model AI tidak boleh menghapus data pengguna tanpa batasan yang kuat.

Mendaftarkan alat di server MCP

Lingkaran registrasi menyatukan semuanya. Ini mengulangi daftar alat yang dikurasi, membangun skema Zod dari spesifikasi OpenAPI, dan mendaftarkan setiap alat dengan deskripsi dan anotasinya:

// server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { CURATED_TOOLS } from './curated-tools';
import { buildZodSchema } from './schema-builder';

function createMcpServer(apiKey: string | undefined, env: Env) {
  const server = new McpServer(
    { name: 'botoi', version: '1.0.0' },
    { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() }
  );

  for (const [toolName, tool] of Object.entries(CURATED_TOOLS)) {
    const zodSchema = buildZodSchema(tool.path, tool.method);

    server.registerTool(toolName, {
      title: tool.title,
      description: tool.description,
      inputSchema: zodSchema,
      annotations: tool.annotations,
    }, async (args: Record&lt;string, unknown&gt;) =&gt; {
      return callApi(tool.path, tool.method, args, apiKey, env);
    });
  }

  return server;
}

Saat model memanggil alat, fungsi handler menerima argumen yang diurai dan meneruskannya ke rute API internal. Itu callApi fungsi membuat permintaan HTTP internal dan mengembalikan respons sebagai konten berformat MCP:

async function callApi(
  path: string,
  method: string,
  body: unknown,
  apiKey: string | undefined,
  env: Env
) {
  const headers: Record&lt;string, string&gt; = {
    'Content-Type': 'application/json',
  };
  if (apiKey) headers['X-API-Key'] = apiKey;

  const req = new Request(\`http://internal\$\{path}\`, {
    method: method.toUpperCase(),
    headers,
    body: method === 'post' ? JSON.stringify(body) : undefined,
  });

  const res = appFetcher
    ? await appFetcher(req, env)
    : await fetch(req);

  const json = await res.json();
  if (!json.success) {
    return {
      content: [{ type: 'text', text: JSON.stringify(json.error, null, 2) }],
      isError: true,
    };
  }
  return {
    content: [{ type: 'text', text: JSON.stringify(json.data, null, 2) }],
  };
}

Itu appFetcher pola memungkinkan server MCP memanggil rute API melalui referensi fungsi internal alih-alih membuat permintaan HTTP eksternal. Hal ini untuk menghindari bolak-balik jaringan. Penangan MCP dan rute API dijalankan di Cloudflare Worker yang sama, sehingga perutean internal adalah panggilan fungsi, bukan lompatan HTTP.

Transportasi HTTP tanpa kewarganegaraan di Cloudflare Workers

MCP mendukung dua transport: stdio (untuk proses lokal) dan Streamable HTTP (untuk server jarak jauh). Kami memilih Streamable HTTP karena server berjalan pada Cloudflare Workers, yang tidak mendukung proses yang berjalan lama.

// Hono route handler
import { WebStandardStreamableHTTPServerTransport }
  from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';

app.all('/mcp', async (c) =&gt; {
  const apiKey =
    c.req.header('X-API-Key') ||
    c.req.header('Authorization')?.replace('Bearer ', '');

  const server = createMcpServer(apiKey, c.env);
  const transport = new WebStandardStreamableHTTPServerTransport();
  await server.connect(transport);
  return transport.handleRequest(c.req.raw);
});

Setiap permintaan menciptakan permintaan baru McpServer contoh. Tidak ada status sesi yang bertahan di antara permintaan. Ini bagus karena setiap pemanggilan alat bersifat mandiri; model mengirimkan nama alat dan argumen, dan server mengembalikan hasilnya. Tidak ada transaksi multi-langkah.

Desain tanpa kewarganegaraan memiliki tiga keunggulan:

  • Tidak diperlukan penyimpanan sesi (tanpa Redis, tanpa KV, tanpa database)
  • Berskala ke nol saat idle, menskala secara horizontal saat ada beban
  • Disebarkan ke 300+ lokasi edge Cloudflare tanpa konfigurasi

Penanganan kunci API terjadi di lapisan MCP. Klien mengirimkan kunci melalui X-API-Key atau Authorization: Bearer tajuk. Rute MCP mengekstraknya dan meneruskannya ke panggilan API internal. Tidak ada middleware autentikasi terpisah pada rute MCP itu sendiri.

Buku pedoman untuk API Anda sendiri

Jika Anda memiliki spesifikasi OpenAPI dan ingin membangun server MCP, berikut versi ringkasnya:

  1. Susun daftar alat Anda. Pilih titik akhir 20-80 yang mengembalikan data terstruktur dan menerima masukan sederhana. Lewati titik akhir yang mengembalikan data biner, memerlukan unggahan file, atau memiliki skema masukan yang mendalam.
  2. Tulis konverter skema. Petakan tipe properti OpenAPI Anda ke Zod. Membawa deskripsi, default, dan nilai enum. Tangani pola isi permintaan (POST) dan parameter kueri (GET).
  3. Tulis deskripsi dua kalimat. Kalimat pertama: apa yang dilakukan alat tersebut, dimulai dengan kata kerja. Kalimat kedua: "Gunakan saat" + kondisi pemicu. Spesifik tentang tipe dan format data.
  4. Tambahkan anotasi. Tandai alat hanya-baca. Tandai alat yang melakukan panggilan jaringan eksternal. Identifikasi operasi idempoten. Kecualikan alat yang merusak kecuali Anda memiliki alur konfirmasi.
  5. Pilih transportasi Anda. Gunakan HTTP yang Dapat Dialirkan untuk server jarak jauh, stdio untuk alat CLI lokal. MCP SDK menyediakan keduanya.
  6. Rutekan panggilan alat ke API Anda yang ada. Jangan menulis ulang logika bisnis. Hubungi rute Anda sendiri secara internal. Server MCP adalah lapisan adaptor tipis.

Server MCP Botoi terdiri dari 4 file: curated-tools.ts (49 definisi alat), schema-builder.ts (Konverter OpenAPI-ke-Zod), server.ts (pendaftaran dan perutean), dan tools.ts (titik akhir manifes publik). Semuanya menambahkan sekitar 400 baris TypeScript ke API yang sudah ada.

Cobalah

Server Botoi MCP aktif https://api.botoi.com/mcp. Hubungkan ke Claude Desktop, Claude Code, Cursor, atau VS Code dalam waktu kurang dari satu menit. Lihat dokumen pengaturan MCP untuk cuplikan konfigurasi untuk setiap klien yang didukung.

Telusuri manifes alat lengkap untuk melihat 49 definisi alat beserta skema dan anotasinya. Itu dokumen API mencakup set lengkap 150+ titik akhir REST di belakang server MCP.

FAQ

Bagaimana Anda membangun server MCP dari spesifikasi OpenAPI?
Parsing definisi jalur OpenAPI Anda, ekstrak skema isi permintaan (untuk POST) atau parameter kueri (untuk GET), konversi setiap properti ke tipe Zod, lalu daftarkan setiap alat pada instance McpServer dengan skema Zod sebagai inputSchema. MCP SDK menangani transportasi JSON-RPC dan penemuan alat.
Mengapa tidak mengekspos semua endpoint API sebagai alat MCP?
Model AI memiliki batas jendela konteks. Setiap definisi alat menggunakan token. Manifes berisi 150 alat dapat memakan 30.000+ token sebelum percakapan dimulai. Mengkurasi hingga 49 alat bernilai tinggi menjaga manifes tetap di bawah 8.000 token dan meningkatkan akurasi pemilihan alat.
Apa yang dimaksud dengan anotasi alat MCP dan mengapa itu penting?
Anotasi adalah petunjuk metadata seperti readOnlyHint, destruktifHint, idempotentHint, dan openWorldHint. Mereka memberi tahu model AI apakah suatu alat membaca atau menulis data, apakah alat tersebut menghubungi layanan eksternal, dan apakah aman untuk mencoba kembali. Model menggunakan petunjuk ini untuk merencanakan alur kerja multi-langkah dan menghindari tindakan merusak tanpa konfirmasi.
Bisakah server MCP berjalan di Cloudflare Workers?
Ya. Gunakan WebStandardStreamableHTTPServerTransport dari MCP SDK. Ia bekerja dengan runtime apa pun yang mendukung API Permintaan/Respon Standar Web. Cloudflare Workers, Deno Deploy, dan Vercel Edge Functions semuanya memenuhi syarat. Setiap permintaan membuat instance McpServer baru, sehingga tidak diperlukan status sesi.
Bagaimana cara menulis deskripsi alat MCP untuk model AI?
Mulailah dengan kata kerja. Nyatakan apa yang dilakukan alat tersebut dalam satu kalimat. Tambahkan kalimat kedua yang dimulai dengan "Gunakan saat" yang menjelaskan kondisi pemicu. Lewati detail penerapan. Contoh: "Kueri data DNS (A, AAAA, MX, TXT, CNAME, NS) untuk sebuah domain. Gunakan saat Anda perlu memeriksa konfigurasi DNS atau memecahkan masalah resolusi domain."

Mulai membangun dengan botoi

150+ endpoint API untuk pencarian, pemrosesan teks, pembuatan gambar, dan utilitas developer. Paket gratis, tanpa kartu kredit.