Handle Server Requests

Purpose and Scope

This page explains how an MCP client safely handles requests that originate from the server. In normal client code, you usually think of the client as the caller: it connects, lists tools, calls tools, reads resources, and fetches prompts. Server-originated requests invert that direction. A server tool may ask the client to collect user input through elicitation, or an older server may ask the client to run a model through sampling. The important rule is capability declaration: a server should only send request methods for capabilities the client advertised, and the SDK enforces that contract on both sides.

Sources: docs/clients/server-requests.md, docs/clients/connect.md, docs/clients/calling.md

A capability is a negotiated statement about what one side of the connection can do. For this page, the client-side capabilities are elicitation and sampling. Elicitation means the server can ask the host application to obtain more input from the user, either by rendering a form from a schema or by opening a URL flow. Sampling means the server can ask the host-controlled model layer to produce a message. Sampling is documented as deprecated for new server design, but clients may still keep a handler for compatibility with servers that have not migrated away from it.

Relevant Source Files

  • docs/clients/server-requests.md — primary how-to for declaring client capabilities and registering handlers for elicitation/create and sampling/createMessage.
  • docs/advanced/low-level-server.md — explains the lower-level setRequestHandler model and why declared capabilities and method handlers are separate protocol concerns.
  • docs/get-started/first-server.md — establishes the tool execution model that can trigger client-facing request flows during a callTool() round trip.
  • docs/clients/caching.md — shows that clients have per-call behavior and SDK-managed client features, which matters when reasoning about handler registration and hidden round trips.
  • docs/clients/calling.md — describes ordinary client calls such as listTools() and callTool(), including the distinction between tool results and protocol-level failures.
  • docs/clients/connect.md — covers Client construction, transport connection, protocol negotiation, server capabilities, and clean shutdown.

Declare Client Capabilities

Declare server-request support when constructing the Client. The constructor receives client identity, then an options object where capabilities advertises request surfaces the client is prepared to answer. A server should not assume an arbitrary host can perform user-interface or model work. Instead, the server sees the negotiated capabilities from initialization and sends only supported request methods. This keeps server behavior explicit and gives the client application a single place to audit high-impact interactions such as asking a user for data or spending model tokens on behalf of a server.

Sources: docs/clients/server-requests.md, docs/clients/connect.md

import { Client } from '@modelcontextprotocol/client';
 
const client = new Client(
    { name: 'my-client', version: '1.0.0' },
    {
        capabilities: {
            sampling: {},
            elicitation: { form: {}, url: {} }
        }
    }
);

Treat the capability object as part of your security and product contract, not as boilerplate. An empty elicitation: {} declares form mode only; URL mode must be listed explicitly as url: {}. That distinction is useful for hosts that can render forms but do not want servers to drive browser redirects. The client package also exposes getSupportedElicitationModes, which turns the elicitation capability object into booleans such as supportsFormMode and supportsUrlMode. Use that helper when shared UI code needs to make mode decisions without re-implementing capability parsing.

Handle Elicitation Requests

A server sends an elicitation request with the method elicitation/create. The request is usually triggered by server-side tool code that needs more user input before it can finish. The handler should branch on request.params.mode. If the mode is url, the request carries a URL that the host application may open in a browser or embedded web view. For all other cases, including older form requests that omit mode, the client should treat the request as a form request and render request.params.requestedSchema for the user.

Sources: docs/clients/server-requests.md

client.setRequestHandler('elicitation/create', async request => {
    if (request.params.mode === 'url') {
        // Open request.params.url in the user's browser; answer when they finish.
        return { action: 'accept' };
    }
 
    // Render request.params.requestedSchema as a form; return what the user entered.
    return { action: 'accept', content: { city: 'Lisbon' } };
});

The response action records the user's decision. accept may carry submitted content; decline and cancel carry no content. Keep that distinction visible in your own UI because it is semantically different for the server. A decline can mean the user intentionally refused to provide information, while cancel often means the flow was interrupted or abandoned. The docs explicitly warn not to branch on mode === 'form', because form requests sent before the mode field existed may omit it. Branch on url, and treat everything else as form-mode elicitation.

Handle Sampling Requests

Sampling uses the method sampling/createMessage. In this flow, the server sends a list of messages, and the client-side host decides how to run those messages through a model it controls. The documented handler logs or inspects the last message, then returns an assistant message with a model name and content block. This pattern lets an MCP host remain in charge of model selection, policy, and credentials even when the request began inside a server tool. However, the docs mark server-driven sampling as deprecated under SEP-2577 and recommend that servers call their LLM provider directly for new implementations.

Sources: docs/clients/server-requests.md

client.setRequestHandler('sampling/createMessage', async request => {
    const lastMessage = request.params.messages.at(-1);
    console.log('Sampling request:', lastMessage?.content);
 
    // In production, run the messages through your model here.
    return {
        model: 'host-model',
        role: 'assistant',
        content: { type: 'text', text: 'One travel mug to Lisbon.' }
    };
});

Keep the handler if you need compatibility with servers that still request sampling. In production, this handler is also a policy boundary. It should validate whether the connected server is allowed to ask for model work, apply user consent rules where appropriate, and route the request to the correct model provider or refuse it. Do not blindly pass arbitrary server prompts to a privileged model context. Because the response becomes part of the server tool's result path, failures should be explicit and observable in the host application rather than hidden as generic transport errors.

Execution Flow and Safety Boundaries

Register each server-request handler once on the Client instance you construct. The same handler answers a request pushed directly by the server and a request the SDK fulfills during a callTool() round. Your application code does not need to distinguish those delivery paths. That design matters because protocol versions may differ in how the request is transported, but the host's policy and UI behavior should remain stable. Put permission checks, user prompts, logging, and model-routing logic inside the handler rather than next to individual tool calls.

Sources: docs/clients/server-requests.md, docs/clients/calling.md, docs/clients/connect.md

The normal connection flow still applies. Create the Client, connect it with a transport such as Streamable HTTP, stdio, SSE fallback, or an in-memory pair for tests, and wait for connect() to complete the initialize handshake. After that, the client has the negotiated protocol version, the server capabilities, and the server instructions. Server-request handling adds the client capabilities in the other direction. In practical host code, read the server's instructions for model prompting, use server capabilities to decide which ordinary client verbs to call, and use your own declared capabilities to constrain what the server may ask of the host.

The low-level server documentation reinforces the same separation between capabilities and handlers. A low-level Server routes JSON-RPC methods to handlers registered with setRequestHandler, but it does not infer capabilities from those handlers. Dropping a capability while keeping the handler is an error-prone mismatch. The same mental model is useful on the client side: declaring sampling or elicitation is not the handler itself, and registering a handler without intentionally declaring the corresponding capability is not a product decision. Keep negotiation and implementation aligned.

Compact API Reference

ComponentConcrete nameUse
Client constructornew Client({ name, version }, { capabilities })Advertise client support for server-originated request methods during initialization.
Elicitation capabilitycapabilities.elicitationDeclare form support with {} or { form: {} }; declare URL support explicitly with { url: {} }.
Sampling capabilitycapabilities.samplingAllow older servers to send sampling/createMessage requests.
Elicitation handlerclient.setRequestHandler('elicitation/create', handler)Render a form or open a URL and return { action } plus optional content.
Sampling handlerclient.setRequestHandler('sampling/createMessage', handler)Run messages through a host-controlled model path and return an assistant message.
Mode helpergetSupportedElicitationModesConvert an elicitation capability object into mode-support booleans.

Testing and Next Steps

For tests, pair a client and server in memory so the same handlers run without a network server or child process. The server-request docs describe examples wired over an in-memory transport pair to a server whose tools elicit input and request sampling. That setup is valuable because it exercises the behavior that matters: a tool call can appear to the caller as one operation while the SDK performs a server-to-client request inside the round trip. Assertions should cover accepted, declined, canceled, URL-mode, omitted-mode form, and sampling compatibility cases.

Sources: docs/clients/server-requests.md, docs/get-started/first-server.md, docs/clients/caching.md

Next, read client-connect to place these handlers in the full connection lifecycle, then client-calling to understand where server-originated requests can appear during ordinary callTool() usage. For server authors, read server-elicitation and server-sampling to understand how tool code triggers these client handlers. If you are building a low-level protocol integration, read low-level-server as a reminder that method handlers, capability declarations, validation, and policy are separate responsibilities that should be kept explicit.