Guide · AI Model Serving & Inference

MCP Server Hugging Face Inference API — pipelines, cold starts, dedicated endpoints, embeddings

Three Hugging Face Inference API behaviours catch MCP server authors off-guard: serverless models return HTTP 503 with an estimated_time field during cold start — you must wait and retry, not fail immediately — when a serverless model is not loaded, the API returns {"error": "Model is currently loading.", "estimated_time": 20} with status 503; retrying immediately causes another 503; you must sleep for estimated_time seconds then retry up to 3 times before surfacing an error to the MCP client; pipeline task type is implicit in the model's card but must match your request shape — a text-generation model expects {"inputs": "...string..."} while a text2text-generation model (T5, BART) expects the same field name but produces output via encoder-decoder and the response shape differs ([{"generated_text": "..."}] vs [{"translation_text": "..."}] for translation tasks); calling a T5 model via the text-generation endpoint succeeds silently but produces garbled output; and serverless and dedicated endpoints have different base URLs and authentication — serverless uses https://api-inference.huggingface.co/models/{model_id} with a Bearer token, while a Dedicated Endpoint URL looks like https://abc123.us-east-1.aws.endpoints.huggingface.cloud and also uses Bearer auth but bills per second of compute instead of per token.

TL;DR

Use @huggingface/inference SDK: const hf = new HfInference(process.env.HF_TOKEN). For text generation call hf.textGeneration({ model, inputs, parameters }). For embeddings call hf.featureExtraction({ model, inputs }). Wrap every call in a retry loop that checks for estimatedTime in the error and sleeps before retrying. For Dedicated Endpoints pass the full endpoint URL as model with hf.endpoint(url). Health probe for Dedicated Endpoints: GET {endpoint_url}/health — serverless has no health endpoint.

Serverless Inference API — text generation and cold start handling

The Hugging Face Serverless Inference API hosts thousands of public models. Requests are billed per token and models are loaded on demand — if a model has not been called recently it will be in a cold state and return 503 until loaded. Free tier has strict rate limits (~300 requests/hour per token). Production MCP servers should either use a Pro/Enterprise HF account or switch to a Dedicated Endpoint once request volume justifies the hourly cost.

import { HfInference, InferenceOutputError } from '@huggingface/inference';
import { z } from 'zod';

const hf = new HfInference(process.env.HF_TOKEN);

// Cold-start-aware wrapper for serverless HF inference
async function hfWithColdStartRetry<T>(
  fn: () => Promise<T>,
  maxAttempts = 3
): Promise<T> {
  let lastErr: unknown;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err: any) {
      lastErr = err;

      // HF cold-start: 503 with estimatedTime — must wait before retry
      // InferenceOutputError has .message that contains the JSON
      const message = err?.message ?? '';
      const coldStartMatch = message.match(/"estimated_time"\s*:\s*(\d+(?:\.\d+)?)/);

      if (coldStartMatch) {
        const waitSeconds = Math.min(parseFloat(coldStartMatch[1]) + 2, 120);
        // Wait for estimated_time + 2s buffer before retrying
        await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
        continue;
      }

      // 503 without estimated_time — transient server error, short backoff
      if (err?.httpStatus === 503 || message.includes('503')) {
        await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 5000));
        continue;
      }

      // 429 rate limit — respect Retry-After header or use backoff
      if (err?.httpStatus === 429) {
        await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 10_000));
        continue;
      }

      // Non-retriable error — rethrow immediately
      throw err;
    }
  }

  throw lastErr;
}

// MCP tool — text generation with cold-start retry
server.tool(
  'hf_generate',
  {
    model:      z.string().min(1).default('mistralai/Mistral-7B-Instruct-v0.2'),
    prompt:     z.string().min(1).max(4000),
    max_tokens: z.number().int().min(1).max(2048).optional().default(256),
    temperature: z.number().min(0).max(2).optional().default(0.7),
  },
  async ({ model, prompt, max_tokens, temperature }) => {
    const result = await hfWithColdStartRetry(() =>
      hf.textGeneration({
        model,
        inputs: prompt,
        parameters: {
          max_new_tokens: max_tokens,
          temperature,
          return_full_text: false,  // return only new tokens, not prompt + generated
          do_sample: temperature > 0,
        },
      })
    );

    return {
      content: [{ type: 'text', text: result.generated_text }],
    };
  }
);

Set return_full_text: false — the default is true, which prepends your prompt to the generated output. For MCP tools returning completion text to an agent, false is almost always what you want. For instruction-tuned models (Mistral-Instruct, Llama-3-Instruct), also wrap your prompt in the model's chat template — the Inference API does not apply chat templates automatically for textGeneration; use hf.chatCompletion() if the model supports the Messages API.

Embeddings for RAG — feature extraction

Embedding models on HF use the feature-extraction pipeline task. The output is a nested array: for a single string input it returns number[][] (one embedding per token) or just number[] depending on whether pooling is applied. Most sentence-transformer models return a flat number[] when called through the Inference API — but zero-shot or raw transformer models return a 2D array; you must call output[0] to get the embedding for the [CLS] token, or average the token embeddings yourself.

// MCP tool — single-text embedding via HF Inference API
server.tool(
  'hf_embed',
  {
    text:  z.string().min(1).max(8192),
    model: z.string().min(1).default('sentence-transformers/all-MiniLM-L6-v2'),
  },
  async ({ text, model }) => {
    const raw = await hfWithColdStartRetry(() =>
      hf.featureExtraction({ model, inputs: text })
    );

    // Normalize output shape — sentence-transformers return float[]
    // raw transformer models return float[][] (tokens × dims)
    let embedding: number[];
    if (Array.isArray(raw[0])) {
      // 2D output — mean pool token embeddings (exclude padding)
      const matrix = raw as number[][];
      const dims = matrix[0].length;
      embedding = Array(dims).fill(0).map((_, d) =>
        matrix.reduce((sum, tok) => sum + tok[d], 0) / matrix.length
      );
    } else {
      embedding = raw as number[];
    }

    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          embedding,
          dimensions: embedding.length,
          model,
        }),
      }],
    };
  }
);

// MCP tool — batch embeddings (up to 64 texts per request on serverless)
server.tool(
  'hf_embed_batch',
  {
    texts: z.array(z.string().min(1).max(512)).min(1).max(64),
    model: z.string().min(1).default('sentence-transformers/all-MiniLM-L6-v2'),
  },
  async ({ texts, model }) => {
    const raw = await hfWithColdStartRetry(() =>
      hf.featureExtraction({ model, inputs: texts })
    );

    // Batch input returns float[][] — one embedding per input text
    const embeddings = raw as number[][];
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          embeddings,
          count:      embeddings.length,
          dimensions: embeddings[0]?.length ?? 0,
          model,
        }),
      }],
    };
  }
);

Dedicated Endpoints — no cold starts, fixed hourly cost

HF Dedicated Endpoints run a single model on a reserved GPU instance. The endpoint URL is assigned at creation time and never changes. Unlike serverless, dedicated endpoints have no cold start delay and no shared rate limits — throughput is bounded only by the GPU. They expose a /health HTTP endpoint that returns 200 when the model is loaded and ready.

import { HfInference } from '@huggingface/inference';

// Dedicated endpoint — use hf.endpoint() to wrap the custom URL
const ENDPOINT_URL = process.env.HF_ENDPOINT_URL!;  // e.g. https://abc123.us-east-1.aws.endpoints.huggingface.cloud
const hfEndpoint   = hf.endpoint(ENDPOINT_URL);

// MCP tool — text generation via Dedicated Endpoint (no cold-start retry needed)
server.tool(
  'hf_endpoint_generate',
  {
    prompt:      z.string().min(1).max(8000),
    max_tokens:  z.number().int().min(1).max(4096).optional().default(512),
    temperature: z.number().min(0).max(2).optional().default(0.7),
  },
  async ({ prompt, max_tokens, temperature }) => {
    // Dedicated endpoints do not cold-start — no retry needed
    const result = await hfEndpoint.textGeneration({
      inputs: prompt,
      parameters: {
        max_new_tokens:  max_tokens,
        temperature,
        return_full_text: false,
        do_sample:       temperature > 0,
      },
    });

    return { content: [{ type: 'text', text: result.generated_text }] };
  }
);

// MCP resource — Dedicated Endpoint health probe
server.resource('hf_endpoint_health', 'hf://endpoint/health', async () => {
  try {
    // Dedicated Endpoints expose GET /health — serverless does NOT
    const res = await fetch(`${ENDPOINT_URL}/health`, {
      headers: { Authorization: `Bearer ${process.env.HF_TOKEN}` },
      signal:  AbortSignal.timeout(10_000),
    });

    const status = res.status === 200 ? 'ok' : 'degraded';
    const body   = res.ok ? await res.json().catch(() => null) : null;

    return {
      contents: [{
        uri: 'hf://endpoint/health',
        text: JSON.stringify({
          status,
          http_status: res.status,
          endpoint:    ENDPOINT_URL,
          model_info:  body,
          checked_at:  new Date().toISOString(),
        }),
      }],
    };
  } catch (err: any) {
    return {
      contents: [{
        uri: 'hf://endpoint/health',
        text: JSON.stringify({ status: 'unreachable', message: err.message }),
      }],
    };
  }
});

For MCP servers in production, prefer Dedicated Endpoints over the Serverless API when your tools are called frequently (more than ~100 times/hour) or when cold-start latency is unacceptable. The break-even point varies by model size: a 7B model endpoint costs roughly $0.50/hour — if your MCP server makes 1,000+ inferences per hour, dedicated is cheaper and more predictable than paying per-token on serverless.