MP-201b · Module 2

API Wrapping

4 min read

REST APIs are the lingua franca of enterprise data, but AI models cannot call them directly. An API-wrapping MCP server bridges this gap: it translates MCP resource reads into HTTP requests, handles authentication, manages pagination, and returns structured data the model can consume. The server acts as a gateway — the AI sees clean, typed MCP resources; the server handles the HTTP plumbing, retry logic, and credential management behind the scenes.

Authentication pass-through is the most sensitive design decision. The MCP server needs credentials to call the upstream API — API keys, OAuth tokens, or service account certificates. These credentials should never be visible to the AI model. The server loads them from environment variables or a secrets manager at startup, uses them in HTTP headers, and strips them from any response data or error messages that flow back to the client. If an API returns an error containing the bearer token in a debug header, the server must sanitize that response before returning it as resource content.

// Wrapping a REST API as MCP resources

const API_BASE = "https://api.example.com/v2";
const API_KEY = process.env.API_KEY!; // Never expose to the model

async function apiFetch(path: string, params?: Record<string, string>) {
  const url = new URL(`${API_BASE}${path}`);
  if (params) {
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  }
  const res = await fetch(url.toString(), {
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      Accept: "application/json",
    },
  });
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}

// Paginated list resource
server.resource(
  "customers",
  new ResourceTemplate("api://customers{?page,limit}", { list: undefined }),
  async (uri, { page = "1", limit = "50" }) => {
    const data = await apiFetch("/customers", { page, limit });
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          items: data.results,
          total: data.total_count,
          page: Number(page),
          hasMore: data.has_next_page,
        }),
      }],
    };
  }
);

// Detail resource
server.resource(
  "customer-detail",
  new ResourceTemplate("api://customers/{customerId}", { list: undefined }),
  async (uri, { customerId }) => {
    const data = await apiFetch(`/customers/${customerId}`);
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(data),
      }],
    };
  }
);
  1. Map endpoints to resources For each API endpoint, decide if it is a static resource (fixed URI) or a template (parameterized). List endpoints become static; detail endpoints become templates.
  2. Handle authentication securely Load credentials from environment variables. Never log, return, or expose them in error messages. Sanitize all API responses before returning as resource content.
  3. Implement pagination controls Expose page and limit as template parameters. Return metadata (total, hasMore) alongside the data so the client can navigate the full dataset without fetching everything at once.