Tutorial

How to make your MCP server ask the model to generate text (sampling is deprecated in the 2026-07-28 spec)

Sampling (sampling/createMessage) was deprecated on 2026-07-28. Here is the pattern that replaces it — a runnable MCP server that asks the model mid-tool using Multi Round-Trip Requests.

MCPOrbit Team

MCP server operations

Published
Updated
· Updated
Read time
· 10 min read
A tool call pausing to ask the client to run a model completion, then resuming with the answer — the MRTR round trip that replaces MCP sampling.

If you want your MCP server to ask the model to generate text mid-tool — summarize a document, classify an input, draft a reply — do not reach for sampling/createMessage. As of the 2026-07-28 specification it is deprecated under SEP-2577. The replacement is Multi Round-Trip Requests (MRTR, SEP-2322): your tool returns resultType "input_required" with a sampling request, the client runs the completion, and it re-issues the same tool call with the answer attached under inputResponses. Below is a server that does exactly that, with code you can run.

Is MCP sampling deprecated?

Yes. As of the 2026-07-28 specification, sampling (sampling/createMessage), along with Roots (roots/list) and Logging, is deprecated under SEP-2577. It keeps working — the spec ships a formal deprecation policy with a twelve-month minimum window — but new servers should not adopt it. The reason is structural: the 2026-07-28 core is stateless, and sampling required the server to hold open a bidirectional stream so it could call back into the client. A stateless server behind a round-robin load balancer cannot rely on that stream existing.

What replaces sampling in the 2026-07-28 spec?

MRTR (SEP-2322). Instead of the server pushing a sampling/createMessage request to the client over a held-open stream, the tool call itself pauses and asks for what it needs. The server returns a result with resultType "input_required" carrying the requests it wants answered; the client fulfills them (for sampling, it runs a model completion) and re-issues the original tool call with the answers attached under inputResponses. The three primitives that used to be server-initiated — elicitation, sampling, and roots — now all ride this one round-trip pattern.

MRTR replaces the server-initiated elicitation/create, sampling/createMessage, and roots/list requests that previously required a held-open stream.

The 2026-07-28 MCP specification

The old way: a tool that called sampling/createMessage

Under the pre-2026-07-28 pattern, a tool asked the model directly through the server's sampling capability. It read cleanly, but it only worked because a single long-lived connection tied one client to one server process:

// DEPRECATED as of 2026-07-28 (SEP-2577). Do not build new servers on this.
server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name !== "summarize") throw new Error("unknown tool");

  // Server calls back into the client over a held-open stream.
  const completion = await server.createMessage({
    messages: [
      {
        role: "user",
        content: { type: "text", text: `Summarize:

${req.params.arguments.text}` },
      },
    ],
    maxTokens: 200,
  });

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

The new way: a summarize tool built on MRTR

We will build a summarize tool that needs the model to write the summary. Under MRTR the same tool runs in two passes. On the first pass it has no completion yet, so it returns input_required with a sampling request. The client runs the completion and calls the tool again; on the second pass the completion arrives in inputResponses and the tool returns the final summary. No stream is held open between the two passes — each is an ordinary stateless request.

The server

// summarize-server.ts
// Pinned: @modelcontextprotocol/[email protected], [email protected]
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

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

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "summarize",
      description: "Summarize a block of text in one sentence.",
      inputSchema: {
        type: "object",
        properties: { text: { type: "string" } },
        required: ["text"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const { name, arguments: args, inputResponses } = req.params;
  if (name !== "summarize") throw new Error(`Unknown tool: ${name}`);

  // Pass 2: the client ran the model and retried with the completion.
  if (inputResponses?.summary) {
    const completion = inputResponses.summary;
    const text = completion.content?.type === "text" ? completion.content.text : "";
    return { resultType: "complete", content: [{ type: "text", text: text.trim() }] };
  }

  // Pass 1: we need the model. Ask the client to run a completion (MRTR).
  return {
    resultType: "input_required",
    inputRequests: [
      {
        key: "summary",
        method: "sampling/createMessage",
        params: {
          messages: [
            {
              role: "user",
              content: {
                type: "text",
                text: `Summarize the following in one sentence:

${args.text}`,
              },
            },
          ],
          maxTokens: 200,
        },
      },
    ],
  };
});

await server.connect(new StdioServerTransport());

The client driving the round trip

// client.ts
let res = await client.callTool({ name: "summarize", arguments: { text: LONG } });

while (res.resultType === "input_required") {
  const inputResponses = {};
  for (const reqd of res.inputRequests) {
    if (reqd.method === "sampling/createMessage") {
      inputResponses[reqd.key] = await runModel(reqd.params);
    }
  }
  res = await client.callTool({
    name: "summarize",
    arguments: { text: LONG },
    inputResponses,
  });
}

console.log(res.content[0].text);

What changes when you migrate sampling to MRTR

  • Control inverts. The client, not the server, owns the model call — it decides which model, applies its own rate limits, and can refuse. Your server just describes the completion it wants.
  • State goes on the wire. Anything the second pass needs must round-trip through inputResponses; you cannot stash it in server memory keyed to a connection.
  • One loop handles everything. Elicitation (ask the human) and sampling (ask the model) are now the same input_required loop with a different method — write the client loop once.
  • It survives a load balancer. Because each pass is a plain stateless request, sampling now works behind round-robin routing, which is the whole point of the 2026-07-28 core.

Run it yourself

Clone-free — the two files above are the whole example. Install the pinned SDK, wire runModel to your provider (any chat completion API works; the params carry messages and maxTokens), and start the server over stdio:

npm i @modelcontextprotocol/[email protected] [email protected]
npx tsx summarize-server.ts

FAQ

Is sampling being removed from MCP?
No. As of 2026-07-28 it is deprecated under SEP-2577 with a twelve-month minimum support window. It keeps working, but new servers should use MRTR instead of sampling/createMessage.
Why was MCP sampling deprecated?
The 2026-07-28 protocol core is stateless. Sampling required the server to hold open a bidirectional stream to call back into the client, which breaks behind a round-robin load balancer. MRTR moves the request into the tool-call round trip so no stream is needed.
What is the difference between sampling and elicitation now?
Both are MRTR input requests returned with resultType "input_required". Sampling asks the model to generate text; elicitation asks the human user for structured input. Same round trip, different method name in inputRequests.
Do I have to migrate my sampling server before 2026-07-28?
No. The 2026-07-28 date is the spec release, not a sampling cutoff. Sampling has its own twelve-month deprecation runway, so migrate to MRTR when it is convenient rather than as an emergency.
How does the client know it needs to run a completion?
Your tool returns resultType "input_required" with an inputRequests entry whose method is sampling/createMessage. The client runs that completion and retries the original tool call with the result under inputResponses, keyed to match the request.

About the author

MCPOrbit Team

MCP server operations

MCPOrbit runs fleet observability for Model Context Protocol servers in production. We track every spec change against real server traffic.

Share this post

MCPOrbit

Test an MCP server in 60 seconds.

Download MCPOrbit for free. No signup, no telemetry. Hear about a server and test it before the curiosity wears off.

macOS 14+ · Apple Silicon & Intel · No account needed