# How to implement sampling (LLM calls) in MCP

- Tool: MCP
- Difficulty: Advanced
- Time required: 30-40 min
- Compatibility: MCP TypeScript SDK 1.x, clients that support sampling capability
- Last updated: March 2026

## TL;DR

Sampling in MCP lets your server request the AI client to generate text completions on the server's behalf. Use the sampling/createMessage request to ask the client's AI model to analyze data, summarize content, or make decisions as part of a tool workflow. This enables agentic patterns where the server uses AI capabilities without having its own model.

## Implementing Sampling in MCP Servers

Sampling is an advanced MCP capability that inverts the usual flow. Instead of the AI client calling tools on your server, your server asks the AI client to generate a completion. The server sends a sampling/createMessage request with messages and optional model preferences, the client routes it to its AI model, and returns the generated text.

This enables powerful patterns: a tool can analyze data using AI without bundling its own model, a workflow can combine database queries with AI-powered summarization, and multi-step agents can reason about intermediate results. The key constraint is that sampling requires client consent — the human user must approve the request.

## Before you start

- A working MCP server with the TypeScript or Python SDK
- An MCP client that supports the sampling capability (e.g., Claude Desktop)
- Understanding of MCP tool registration and the request/response flow
- Familiarity with LLM prompt design for structured outputs

## Step-by-step guide

### 1. Understand the sampling flow

In normal MCP flow, the client calls tools on the server. With sampling, the server sends a sampling/createMessage request to the client, asking it to generate a completion. The client presents this to the user for approval (human-in-the-loop), routes it to its AI model, and returns the generated message. The server can then use this AI-generated content in its tool response.

```
// The sampling flow:
// 1. Client calls tool on server
// 2. Server needs AI analysis as part of the tool
// 3. Server sends sampling/createMessage to client
// 4. Client shows user approval dialog
// 5. Client routes to its AI model
// 6. Client returns generated message to server
// 7. Server uses the result in its tool response
```

**Expected result:** You understand that sampling is a server-to-client request for AI completions, with human approval.

### 2. Send a sampling request from a tool handler

Inside a tool handler, use the server's request method to send a sampling/createMessage request. Provide the messages array (conversation for the AI), optional model preferences, and a system prompt. The client returns a message with the AI's response. Access the sampling capability through the extra parameter in your tool handler.

```
// TypeScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "sampling-server", version: "1.0.0" });

server.registerTool("analyze-data", {
  description: "Analyze a dataset and return AI-generated insights",
  inputSchema: {
    data: z.string().describe("Data to analyze (JSON or CSV format)"),
    question: z.string().describe("Specific question about the data"),
  },
}, async ({ data, question }, extra) => {
  // Request the client's AI model to analyze the data
  try {
    const samplingResult = await extra.requestSampling({
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `Analyze this data and answer the question.\n\nData:\n${data}\n\nQuestion: ${question}\n\nProvide a concise, data-driven answer.`,
          },
        },
      ],
      maxTokens: 1000,
      systemPrompt: "You are a data analyst. Provide precise, factual analysis based only on the data provided.",
    });

    return {
      content: [{
        type: "text",
        text: `Analysis:\n${samplingResult.content.type === "text" ? samplingResult.content.text : "Non-text response"}`,
      }],
    };
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: Sampling not available — ${(error as Error).message}` }],
      isError: true,
    };
  }
});
```

**Expected result:** The tool sends data to the client's AI model for analysis and includes the AI response in its tool result.

### 3. Specify model preferences for sampling

When requesting sampling, you can express preferences about which model the client should use. Specify hints (preferred model names), cost priority, speed priority, and intelligence priority. The client uses these as suggestions — it ultimately decides which model to use based on user settings and availability.

```
// TypeScript — model preferences
const samplingResult = await extra.requestSampling({
  messages: [{
    role: "user",
    content: { type: "text", text: "Summarize this document..." },
  }],
  maxTokens: 500,
  modelPreferences: {
    hints: [
      { name: "claude-sonnet-4-20250514" },  // Preferred model
      { name: "claude" },             // Fallback: any Claude model
    ],
    costPriority: 0.3,           // 0 = cheapest, 1 = most expensive ok
    speedPriority: 0.7,          // 0 = slowest ok, 1 = fastest
    intelligencePriority: 0.5,   // 0 = simplest, 1 = smartest
  },
  systemPrompt: "Provide a brief summary in 2-3 sentences.",
});
```

**Expected result:** The client receives your model preferences and selects the most appropriate available model.

### 4. Build a multi-step tool with sampling

Sampling is most powerful in multi-step tool workflows. A tool can fetch data from a database, use sampling to analyze it, then take action based on the analysis. This creates an agentic loop where the server orchestrates data access and the client's AI provides reasoning.

```
// TypeScript — multi-step tool with sampling
server.registerTool("smart-report", {
  description: "Generate an AI-analyzed report from database data",
  inputSchema: {
    tableName: z.string().describe("Table to analyze"),
    metric: z.string().describe("Metric to focus on"),
  },
}, async ({ tableName, metric }, extra) => {
  try {
    // Step 1: Fetch data from database
    const data = await pool.query(
      `SELECT * FROM ${tableName} ORDER BY created_at DESC LIMIT 100`
    );

    // Step 2: Use sampling to analyze the data
    const analysis = await extra.requestSampling({
      messages: [{
        role: "user",
        content: {
          type: "text",
          text: `Analyze this data focusing on ${metric}:\n\n${JSON.stringify(data.rows, null, 2)}\n\nProvide:\n1. Key trends\n2. Anomalies\n3. Recommendations`,
        },
      }],
      maxTokens: 2000,
      systemPrompt: "You are a business analyst. Be specific and reference actual data values.",
    });

    const analysisText = analysis.content.type === "text" ? analysis.content.text : "";

    // Step 3: Return combined result
    return {
      content: [{
        type: "text",
        text: `Report for ${tableName} — ${metric}\n\nData points: ${data.rowCount}\n\n${analysisText}`,
      }],
    };
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
      isError: true,
    };
  }
});
```

**Expected result:** The tool fetches database data, sends it to the AI for analysis, and returns a combined report.

### 5. Handle clients that do not support sampling

Not all MCP clients support sampling. Check the client's capabilities during initialization or handle the error gracefully when sampling fails. Provide a fallback path that returns raw data without AI analysis. For production servers that depend heavily on sampling, RapidDev can help design fallback strategies and client compatibility testing.

```
// TypeScript — graceful sampling fallback
server.registerTool("summarize-logs", {
  description: "Summarize application logs, with AI analysis if available",
  inputSchema: {
    hours: z.number().min(1).max(72).default(24)
      .describe("Hours of logs to analyze"),
  },
}, async ({ hours }, extra) => {
  const logs = await fetchLogs(hours);
  const summary = {
    totalEntries: logs.length,
    errors: logs.filter((l: any) => l.level === "error").length,
    warnings: logs.filter((l: any) => l.level === "warn").length,
  };

  // Try AI analysis, fall back to raw summary
  let analysis = "AI analysis not available (client does not support sampling).";
  try {
    const result = await extra.requestSampling({
      messages: [{
        role: "user",
        content: {
          type: "text",
          text: `Analyze these application logs and identify patterns:\n\n${JSON.stringify(logs.slice(0, 50), null, 2)}`,
        },
      }],
      maxTokens: 1000,
    });
    if (result.content.type === "text") {
      analysis = result.content.text;
    }
  } catch {
    // Sampling not supported or declined — use raw summary
  }

  return {
    content: [{
      type: "text",
      text: `Log Summary (last ${hours}h):\n${JSON.stringify(summary, null, 2)}\n\nAnalysis:\n${analysis}`,
    }],
  };
});
```

**Expected result:** The tool works with any client: AI-powered analysis when sampling is available, raw summary when it is not.

## Complete code example

File: `src/index.ts`

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "sampling-demo-server",
  version: "1.0.0",
});

// Tool with sampling for AI analysis
server.registerTool("analyze-text", {
  description: "Analyze text content using AI (requires client sampling support)",
  inputSchema: {
    text: z.string().min(1).describe("Text to analyze"),
    analysisType: z.enum(["sentiment", "summary", "key-points", "entities"])
      .describe("Type of analysis to perform"),
  },
}, async ({ text, analysisType }, extra) => {
  const prompts: Record<string, string> = {
    sentiment: "Analyze the sentiment of this text. Rate as positive/negative/neutral with confidence.",
    summary: "Summarize this text in 2-3 sentences.",
    "key-points": "Extract the 3-5 most important points from this text.",
    entities: "Extract all named entities (people, places, organizations, dates) from this text.",
  };

  try {
    const result = await extra.requestSampling({
      messages: [{
        role: "user",
        content: {
          type: "text",
          text: `${prompts[analysisType]}\n\nText:\n${text}`,
        },
      }],
      maxTokens: 1000,
      modelPreferences: {
        intelligencePriority: 0.7,
        speedPriority: 0.5,
        costPriority: 0.3,
      },
    });

    const responseText = result.content.type === "text"
      ? result.content.text
      : "Non-text response received";

    return {
      content: [{
        type: "text",
        text: `${analysisType.toUpperCase()} Analysis:\n\n${responseText}`,
      }],
    };
  } catch (error) {
    return {
      content: [{
        type: "text",
        text: `Error: Sampling failed — ${(error as Error).message}. This tool requires a client that supports MCP sampling.`,
      }],
      isError: true,
    };
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Sampling demo server running on stdio");
```

## Common mistakes

- **Assuming all clients support sampling** — undefined Fix: Always handle the case where sampling is not available. Provide a fallback or clear error message.
- **Sending too much data in sampling messages** — undefined Fix: AI models have token limits. Truncate or summarize data before including it in sampling messages. Send the most relevant subset.
- **Not setting maxTokens on sampling requests** — undefined Fix: Always set maxTokens to prevent excessive token usage. Use the minimum needed for your analysis type.
- **Treating model hints as requirements** — undefined Fix: The hints array is advisory. The client chooses the model. Design prompts that work across different models.
- **Using sampling for simple transformations** — undefined Fix: If you can do the operation with code (regex, math, string manipulation), do it in the handler. Reserve sampling for tasks that genuinely need AI reasoning.

## Best practices

- Always wrap sampling requests in try/catch with fallback behavior
- Set appropriate maxTokens to control cost and response time
- Use systemPrompt to constrain the AI's response format and behavior
- Truncate large datasets before sending in sampling messages
- Use sampling for analysis, summarization, and reasoning — not string formatting
- Provide model hints as suggestions, not requirements
- Design tools to return useful results even when sampling is unavailable
- Test with the MCP Inspector which simulates sampling responses

## Frequently asked questions

### What is the difference between sampling and just calling an LLM API directly?

Sampling uses the client's AI model, which means the server does not need its own API key, the user controls which model is used, and the human-in-the-loop approval ensures the server cannot use AI without consent. Calling an LLM directly requires your own API key and bypasses client control.

### Does the user have to approve every sampling request?

The protocol requires human-in-the-loop approval. In practice, clients may implement automatic approval for trusted servers or let users set approval policies. But your server should assume approval is required and handle rejection gracefully.

### Can I chain multiple sampling requests in one tool?

Yes. A tool can make multiple sequential sampling requests, using each response to inform the next. This enables multi-step reasoning. However, each request may require separate user approval, so minimize the number of calls.

### Which MCP clients support sampling?

Client support for sampling is still evolving. Check the client's capabilities object during initialization. Claude Desktop supports sampling. Other clients may add support over time.

### Can I use sampling in Python FastMCP?

Yes. Use the context object (ctx: Context parameter) to access sampling. Call await ctx.sample() with your messages and model preferences. The pattern is the same as TypeScript but with Python syntax.

### How much does sampling cost?

Sampling uses the client's AI model, so token costs are billed to the client/user, not the server. This is one advantage of sampling over direct API calls — the server incurs no model costs. For enterprise MCP deployments where sampling costs need tracking, RapidDev can help implement usage monitoring.

---

Source: https://www.rapidevelopers.com/mcp-tutorial/how-to-implement-sampling-in-mcp
© RapidDev — https://www.rapidevelopers.com/mcp-tutorial/how-to-implement-sampling-in-mcp
