Tools, Resources, Prompts & Elicitation
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against the 2026-07-28 revision of
the specification, the Python mcp SDK 2.2.0, @modelcontextprotocol/server 2.2.0 with Zod 4.6, and the MCP Java
SDK 2.0.1 with Spring AI 2.0.1 (annotations in org.springframework.ai.mcp.annotation), as checked on 2026-09-29
(see the Model Context Protocol baseline). Python examples target Python 3.12, Java examples
Java 21 and Spring Boot 4.1, and TypeScript examples Node.js 24. The Java SDK and Spring AI speak protocol 2025-11-25
only, so the Java column of every example below has no elicitation through input_required and no caching hints.
An MCP server offers three kinds of content, and the protocol keeps them apart by who decides when they are used.
Tools are chosen by the model, resources by the application, prompts by the user. A fourth mechanism, elicitation,
runs in the opposite direction: the server asks the user for input while it is handling a request. This page covers
all four, plus the freshness hints on list results and the client features the 2026-07-28 revision deprecated. Every
primitive is shown as raw JSON-RPC first, so you see what any SDK is producing, and then as the same
docs-assistant feature in the three stacks. The requests omit the _meta block described on
MCP Architecture; every real request must carry it.
| Primitive | Controlled by | Purpose | docs-assistant example |
|---|---|---|---|
Tool |
The model, with the host asking the user to confirm sensitive calls |
Do something or compute something: query, call an API, change state. |
|
Resource |
The application (the host decides what to attach as context) |
Expose data addressed by a URI: files, records, pages. |
|
Prompt |
The user (typically a slash command or menu entry) |
A reusable, parameterised message template. |
|
Elicitation |
The server asks; the user answers through the client |
Collect a missing value or a confirmation in the middle of a request. |
"Which component should I search?" when |
Tools
A tool is a named operation with a description and a JSON Schema for its arguments. The model reads the name,
description and schema (see Structured Output & Tool
Calling for how a host turns them into function calling), decides to call it, and the host sends tools/call.
tools/list returns the definitions; a server that declares the tools capability must answer it, in a deterministic
order so clients and LLM prompt caches can rely on it.
title keywords are removed){
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"tools": [
{
"name": "search_docs",
"description": "Search the documentation of one component and return the best matching pages.",
"annotations": { "readOnlyHint": true, "openWorldHint": false },
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"component": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null },
"top_k": { "type": "integer", "default": 3 }
},
"required": ["query"]
},
"outputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"component": { "type": "string" },
"hits": { "type": "array", "items": { "$ref": "#/$defs/Hit" } }
},
"required": ["query", "component", "hits"],
"$defs": {
"Hit": {
"type": "object",
"properties": {
"component": { "type": "string" }, "page": { "type": "string" }, "title": { "type": "string" },
"score": { "type": "integer" }, "snippet": { "type": "string" }
},
"required": ["component", "page", "title", "score", "snippet"]
}
}
}
}
],
"ttlMs": 0,
"cacheScope": "private"
}
}
inputSchema and outputSchema are full JSON Schema, and 2026-07-28 makes the dialect explicit: when a schema has no
$schema keyword it is JSON Schema 2020-12, so $defs, $ref, anyOf
and the rest work as the example shows. inputSchema must be an object schema and must not be null; for a tool with
no parameters the specification recommends { "type": "object", "additionalProperties": false }. Servers must
validate every input against it before doing anything, because the model, not a compiler, produced the arguments. A
property may also carry an x-mcp-header keyword that mirrors its value into an HTTP header, which is a transport
feature covered on Transports.
Results, structuredContent and errors
A successful call returns content (text, image, audio, resource links or embedded resources) and, when the tool has
structured output, structuredContent. That field can hold any JSON value — object, array, string, number, boolean or
null — and if the tool declares an outputSchema the server must produce output that conforms to it and clients
should validate it. For compatibility with clients that only read content, a tool that returns structured content
should also return the serialised JSON in a text block. It is captured below from the Python server:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": { "name": "search_docs",
"arguments": { "query": "matrix decomposition", "component": "algebra", "top_k": 2 },
"_meta": { ... } } }
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"isError": false,
"content": [{ "type": "text", "text": "{ \"query\": \"matrix decomposition\", ... }" }],
"structuredContent": {
"query": "matrix decomposition",
"component": "algebra",
"hits": [
{ "component": "algebra", "page": "matrices", "title": "Matrices", "score": 2,
"snippet": "Matrix decompositions such as LU, QR and SVD solve linear systems." },
{ "component": "algebra", "page": "wrappers", "title": "Wrappers", "score": 1,
"snippet": "Wrapper classes hide the decomposition details behind a simple solver." }
]
}
}
}
Errors use two channels and the difference matters, because they are handled by different readers. A protocol
error means the request itself was wrong in a way a model is unlikely to fix — an unknown tool, a request that does
not match the schema, a server fault — and is a JSON-RPC error, for example -32602. A tool execution error
means the tool ran and failed in a way the model can act on: an API failure, an invalid date, a business rule. It is a
normal result with isError: true and a message the model can read and use to retry. Clients should pass tool
execution errors to the model and may show protocol errors to it, though those rarely help. The Python server returns
input-validation failures the way the specification describes, as tool execution errors:
{
"jsonrpc": "2.0",
"id": 8,
"result": {
"resultType": "complete",
"isError": true,
"content": [{ "type": "text",
"text": "Error executing tool search_docs: 2 validation errors for search_docs..." }]
}
}
SDKs differ on the edges. The specification classes an unknown tool as a protocol error (-32602), while the Python
SDK 2.2.0 answered an unknown tool name with an isError: true result in the verification run; do not rely on either
shape, and write tool descriptions so the model rarely gets there. Write actionable error messages: "unknown component
nope; use algebra, units or statistics" lets the model correct itself, "internal error" does not.
The design rules that follow from this are on Designing Good MCP
Servers.
Annotations are hints, not guarantees
A tool may carry annotations that describe its behaviour. They are optional hints for clients deciding how to
present and gate a tool:
| Annotation | Meaning | Default |
|---|---|---|
|
The tool does not modify its environment. |
|
|
The tool may make destructive updates; |
|
|
Repeating the call with the same arguments has no further effect. Meaningful only when |
|
|
The tool may interact with an open world of external entities (a web search does, a memory tool does not). |
|
The specification is blunt that all of these are hints that need not describe the tool faithfully, that clients must
treat annotations as untrusted unless they come from a trusted server, and that clients "should never make tool use
decisions" on annotations from an untrusted one. A malicious server can label a destructive tool readOnlyHint: true.
Use annotations to improve the user experience — a host may skip a confirmation dialog for a read-only tool of a
server it trusts — and never as the enforcement point. Enforcement belongs in the server’s own permission checks and,
for remote servers, in Authorization; the threat model is on
MCP Security. The specification also asks for a human in the loop with the ability to deny
tool invocations (see Human-in-the-Loop).
The tool in three stacks
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.server.mcpserver import Elicit, Resolve
from mcp.types import ToolAnnotations
import store
mcp = MCPServer("docs-assistant", version="1.0.0")
class Hit(BaseModel):
component: str
page: str
title: str
score: int
snippet: str
class SearchResult(BaseModel):
query: str
component: str
hits: list[Hit]
class ComponentChoice(BaseModel):
component: Literal["algebra", "units", "statistics"] = Field(description="Which component to search")
async def choose_component(component: str | None) -> ComponentChoice | Elicit[ComponentChoice]:
if component:
return ComponentChoice(component=component) # type: ignore[arg-type]
return Elicit("Which component should I search?", ComponentChoice)
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False))
async def search_docs(
query: str,
choice: Annotated[ComponentChoice, Resolve(choose_component)],
component: str | None = None,
top_k: int = 3,
) -> SearchResult:
"""Search the documentation of one component and return the best matching pages."""
hits = store.search(query, choice.component, top_k)
return SearchResult(query=query, component=choice.component, hits=[Hit(**h) for h in hits])
The return annotation SearchResult becomes both the outputSchema and the structuredContent; the docstring becomes
the description. store is the stub index from the sub-section’s example code (the real implementation queries pgvector,
see Exposing RAG through MCP). The Resolve / Elicit pair is the elicitation
mechanism described further down. See the Python SDK documentation.
@Component
public class DocsTools {
private final DocStore store;
public DocsTools(DocStore store) {
this.store = store;
}
@McpTool(name = "search_docs",
description = "Search the documentation of one component and return the best matching pages.",
generateOutputSchema = true,
annotations = @McpTool.McpAnnotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false))
public DocStore.SearchResult searchDocs(
@McpToolParam(description = "Free-text query", required = true) String query,
@McpToolParam(description = "algebra, units or statistics", required = true) String component,
@McpToolParam(description = "Maximum number of hits", required = false) Integer topK) {
if (!DocStore.COMPONENTS.contains(component)) {
throw new IllegalArgumentException("Unknown component '" + component + "'");
}
return store.search(query, component, topK == null ? 3 : topK);
}
}
@McpTool from Spring AI’s org.springframework.ai.mcp.annotation package registers the method; with
generateOutputSchema = true the return type produces the outputSchema and the result is returned as
structuredContent. This server ran against Spring Boot 4.1.1 with Spring AI 2.0.1 and passed the same contract
checks as the other two. See Spring Boot MCP Servers and the
Spring AI MCP annotations reference.
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
import { COMPONENTS, search } from './store.js';
const hitSchema = z.object({ component: z.string(), page: z.string(), title: z.string(), score: z.number(), snippet: z.string() });
export function createDocsAssistant(): McpServer {
const server = new McpServer({ name: 'docs-assistant', version: '1.0.0' });
server.registerTool(
'search_docs',
{
description: 'Search the documentation of one component and return the best matching pages.',
inputSchema: z.object({
query: z.string(),
component: z.string().optional(),
top_k: z.number().int().min(1).max(10).default(3)
}),
outputSchema: z.object({ query: z.string(), component: z.string(), hits: z.array(hitSchema) }),
annotations: { readOnlyHint: true, openWorldHint: false }
},
async ({ query, component, top_k }) => {
if (component === undefined || !(COMPONENTS as readonly string[]).includes(component)) {
return { isError: true, content: [{ type: 'text', text: `Unknown component '${component}'` }] };
}
const output = { query, component, hits: search(query, component, top_k) };
return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
}
);
return server;
}
This is the tool without the elicitation branch (that version is shown below). The Zod schemas are converted to JSON
Schema for inputSchema and outputSchema, and the handler’s structuredContent is validated against the output
schema. It type-checks with tsc --noEmit and passes the contract script. See the
TypeScript SDK documentation.
Resources and URI templates
A resource is data identified by a URI. Where a tool is something the model does, a resource is something the
application reads and attaches as context, for example a file the user selected in a picker. resources/list
enumerates concrete resources and resources/read returns their contents. Servers with many addressable items expose
a resource template instead: an RFC 6570 URI template that the client
fills in. The docs-assistant publishes every documentation page through one template:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"resourceTemplates": [
{ "name": "read_page", "uriTemplate": "docs://{component}/{page}",
"description": "The AsciiDoc source of one documentation page.", "mimeType": "text/asciidoc" }
],
"ttlMs": 0,
"cacheScope": "private"
}
}
{ "jsonrpc": "2.0", "id": 2, "method": "resources/read",
"params": { "uri": "docs://algebra/matrices", "_meta": { ... } } }
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"contents": [
{ "uri": "docs://algebra/matrices", "mimeType": "text/asciidoc",
"text": "= Matrices\n\nMatrix decompositions such as LU, QR and SVD solve linear systems.\n" }
],
"ttlMs": 0,
"cacheScope": "private"
}
}
A read of a resource that does not exist must be a JSON-RPC error, with code -32602 (invalid params) in 2026-07-28,
not an empty contents array, which would be ambiguous. Earlier revisions used -32002, so clients should accept
both. In the verification run the Python SDK 2.2.0 answered a template whose handler raised with -32603; whatever your
SDK does, check what it returns for a missing page. Servers must validate every URI they receive: a template parameter
such as {page} is attacker-influenced input and must never be concatenated into a file path without normalisation
(see MCP Security). A resource’s resources/read result is cacheable, and
resources may declare subscribe so a client can watch specific URIs through subscriptions/listen (see
Transports).
@mcp.resource("docs://{component}/{page}", mime_type="text/asciidoc")
def read_page(component: str, page: str) -> str:
"""The AsciiDoc source of one documentation page."""
return store.read(component, page)
The URI template’s parameters must match the function’s parameter names, and the return value becomes the text of
the contents entry. See the Python SDK documentation.
@McpResource(uri = "docs://{component}/{page}", name = "docs-page",
description = "The AsciiDoc source of one documentation page", mimeType = "text/asciidoc")
public ReadResourceResult readPage(String component, String page) {
return ReadResourceResult.builder(List.of(
TextResourceContents.builder("docs://" + component + "/" + page, store.read(component, page))
.mimeType("text/asciidoc").build())).build();
}
ReadResourceResult, TextResourceContents and the other types come from io.modelcontextprotocol.spec.McpSchema in
the MCP Java SDK; the template variables are bound to the method parameters by name. See the
Spring AI MCP annotations reference.
import { ResourceTemplate } from '@modelcontextprotocol/server';
server.registerResource(
'docs-page',
new ResourceTemplate('docs://{component}/{page}', { list: undefined }),
{ title: 'Documentation page', description: 'The AsciiDoc source of one documentation page', mimeType: 'text/asciidoc' },
async (uri, { component, page }) => ({
contents: [{ uri: uri.href, mimeType: 'text/asciidoc', text: read(String(component), String(page)) }]
})
);
{ list: undefined } declares that the template has no enumerable list. See the
TypeScript SDK documentation.
Prompts
A prompt is a server-defined message template that the user picks, typically as a slash command or a menu entry.
It is the least used primitive, but it suits reusable instructions that belong with the service, such as "answer from
the documentation and cite page URIs". prompts/list describes the prompts and their arguments; prompts/get renders
one:
{ "jsonrpc": "2.0", "id": 3, "result": {
"resultType": "complete",
"prompts": [ { "name": "answer_with_citations",
"description": "Instruct the model to answer from the docs and cite page URIs.",
"arguments": [ { "name": "question", "required": true },
{ "name": "component", "required": false } ] } ],
"ttlMs": 0, "cacheScope": "private" } }
{ "jsonrpc": "2.0", "id": 4, "method": "prompts/get",
"params": { "name": "answer_with_citations",
"arguments": { "question": "How do I solve Ax=b?", "component": "algebra" },
"_meta": { ... } } }
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"description": "Instruct the model to answer from the docs and cite page URIs.",
"messages": [
{ "role": "user",
"content": { "type": "text",
"text": "Answer the question using only search_docs results for component 'algebra'. Cite every claim as docs://algebra/<page>.\n\nQuestion: How do I solve Ax=b?" } }
]
}
}
Prompt content is written by the server, but the user chooses when to use it, which is what "user-controlled" means in
the table above. A prompt’s arguments end up in a message that goes to the model, so untrusted values placed in one are
a prompt-injection path like any other (see Agent Guardrails &
Safety). prompts/get may also return input_required, as tools and resources can.
@mcp.prompt()
def answer_with_citations(question: str, component: str = "algebra") -> str:
"""Instruct the model to answer from the docs and cite page URIs."""
return (f"Answer the question using only search_docs results for component '{component}'. "
f"Cite every claim as docs://{component}/<page>.\n\nQuestion: {question}")
A returned string becomes a single user message. See the Python SDK documentation.
@McpPrompt(name = "answer_with_citations",
description = "Instruct the model to answer from the docs and cite page URIs.")
public GetPromptResult answerWithCitations(
@McpArg(name = "question", required = true) String question,
@McpArg(name = "component", required = false) String component) {
String c = component == null ? "algebra" : component;
String text = "Answer the question using only search_docs results for component '" + c + "'. "
+ "Cite every claim as docs://" + c + "/<page>.\n\nQuestion: " + question;
return GetPromptResult.builder(List.of(new PromptMessage(Role.USER, TextContent.builder(text).build())))
.build();
}
See the Spring AI MCP annotations reference.
server.registerPrompt(
'answer_with_citations',
{
description: 'Instruct the model to answer from the docs and cite page URIs.',
argsSchema: z.object({ question: z.string(), component: z.string().default('algebra') })
},
({ question, component }) => ({
messages: [
{
role: 'user' as const,
content: {
type: 'text' as const,
text: `Answer the question using only search_docs results for component '${component}'. Cite every claim as docs://${component}/<page>.\n\nQuestion: ${question}`
}
}
]
})
);
See the TypeScript SDK documentation.
Elicitation through multi-round-trip requests
Sometimes a server cannot finish a request without asking the user something: which component to search, whether to
proceed with a destructive action, a missing parameter. In protocol versions up to 2025-11-25 the server sent its own
elicitation/create request to the client down an open stream. That needs a live back-channel and connection state,
so 2026-07-28 replaced it with multi-round-trip requests (MRTR), which need neither. A server may answer tools/call,
prompts/get or resources/read — and only those — with an input-required result instead of the final one. The
client gathers the answers, then retries the original request with them attached. Each attempt is a complete,
independent request, so a different server instance can serve the retry.
Every result therefore carries a resultType: complete for a final answer, input_required for an interim one. An
input-required result holds inputRequests, a map from server-chosen keys to request objects (an elicitation/create
form or URL request, and, for the deprecated features, sampling or roots requests), and optionally an opaque
requestState. Here is the round trip with the Python server; the client omits component, so the server needs it:
requestState shortened){
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "input_required",
"inputRequests": {
"__main__:choose_component": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Which component should I search?",
"requestedSchema": {
"type": "object",
"properties": {
"component": { "type": "string", "title": "Component", "description": "Which component to search",
"enum": ["algebra", "units", "statistics"] }
},
"required": ["component"]
}
}
}
},
"requestState": "v1.XpCP-mS7hdlgo4cFd19Rv6uoB..."
}
}
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "search_docs",
"arguments": { "query": "matrix" },
"inputResponses": {
"__main__:choose_component": { "action": "accept", "content": { "component": "algebra" } }
},
"requestState": "v1.XpCP-mS7hdlgo4cFd19Rv6uoB...",
"_meta": { ... }
}
}
The reply is an ordinary complete result with structuredContent. The rules that make this safe are short:
-
The retry gets a different JSON-RPC
id; the keys ininputResponsesare the server’s keys frominputRequests. -
The client must echo
requestStateexactly, and must not parse or change it. A server must treat it as attacker-controlled input: if it influences authorization, resource access or business logic, the server must protect its integrity (an HMAC or authenticated encryption), and should bind it to the authenticated user, a short expiry and the originating request. State that fails verification is rejected. Replay protection is not single use; if a state may be consumed only once, the server has to enforce that itself. -
A server must not send an
inputRequestsentry of a kind the client did not declare in its capabilities, so the Python server only elicits because the request’s_metadeclared"elicitation": { "form": {} }. -
Elicitation has two modes. Form mode collects structured data through a restricted, flat JSON Schema and exposes that data to the client; it must never be used for passwords, API keys, tokens or payment details. URL mode sends the user to an external page for such sensitive interactions, and the data never passes through the client.
| Stack | What the SDK does with requestState |
|---|---|
Python |
|
TypeScript 2.2 |
Applies no protection of its own. |
Java (SDK 2.0.1, Spring AI 2.0.1) |
Not applicable: protocol 2025-11-25 has no |
async def choose_component(component: str | None) -> ComponentChoice | Elicit[ComponentChoice]:
if component:
return ComponentChoice(component=component)
return Elicit("Which component should I search?", ComponentChoice)
@mcp.tool()
async def search_docs(
query: str,
choice: Annotated[ComponentChoice, Resolve(choose_component)],
component: str | None = None,
top_k: int = 3,
) -> SearchResult: ...
A resolver is a dependency the SDK runs before the tool: it either returns the value or returns Elicit(…). On a
2026-07-28 connection the SDK turns Elicit into the input_required result above and re-runs the tool on the retry;
on a legacy connection it asks the client directly, so one implementation serves both eras. The resolved parameter
(choice) must have a different name from the tool argument (component), and the inputRequests key is derived from
the module and resolver name, which is why it reads main:choose_component when the server runs as a script.
Calling ctx.elicit() from inside a tool body works only for legacy clients and fails on a 2026-07-28 connection. See the
Python SDK documentation.
import { acceptedContent, inputRequired, type CallToolResult, type InputRequiredResult } from '@modelcontextprotocol/server';
const componentChoice = z.object({ component: z.enum(COMPONENTS).meta({ title: 'Which component should I search?' }) });
async ({ query, component, top_k }, ctx): Promise<CallToolResult | InputRequiredResult> => {
let chosen = component;
if (chosen === undefined) {
const answer = acceptedContent(ctx.mcpReq.inputResponses, 'component', componentChoice);
if (answer === undefined) {
return inputRequired({
inputRequests: {
component: inputRequired.elicit({ message: 'Which component should I search?', requestedSchema: componentChoice })
}
});
}
chosen = answer.component;
}
// ... validate `chosen`, search, and return structuredContent
}
The handler returns inputRequired(…) until the answer arrives, then reads it with acceptedContent, which validates
the client’s answer against the same Zod schema — inputResponses come from the client and are untrusted. The
ctx.mcpReq.elicitInput push channel of earlier SDK versions throws on a 2026-07-28 request. See the
TypeScript SDK documentation. Clients that must drive this loop themselves
are covered in Building MCP Clients; the Python and TypeScript clients run it for you,
as the example on MCP Architecture shows.
The MCP Inspector CLI declares no elicitation capability, so a tool that elicits fails there with "Client did not
declare the form elicitation capability"; the sub-section’s contract script exercises this branch with curl
instead (see Testing & Debugging).
Cacheable list results
Clients used to re-fetch tools/list on every notification or every conversation. 2026-07-28 lets a server say how
long a result stays fresh. The results of server/discover, tools/list, prompts/list, resources/list,
resources/templates/list and resources/read carry two fields, and every complete result of those methods must
include them:
| Field | Meaning |
|---|---|
|
Milliseconds the client may treat the result as fresh, analogous to |
|
|
A public tool list looks like this, taken from the specification’s example:
{ "jsonrpc": "2.0", "id": 1,
"result": { "resultType": "complete", "tools": [ ... ], "ttlMs": 300000, "cacheScope": "public" } }
A client computes freshness as now < t_received + ttlMs. A listChanged notification received while a cached result
is still fresh invalidates it, so TTL and notifications complement each other. Results produced by a retry with
inputResponses or requestState must never be cached, and input_required results carry no hints at all. Note the
consequence for docs-assistant: the captured results above say "ttlMs": 0, "cacheScope": "private", which is the
Python SDK’s default and the safe one — correct, but it makes every client re-fetch. A server whose tool list is the
same for every user should say so; the Python low-level Server accepts cache_hints={"tools/list": CacheHint(ttl_ms=300000, scope="public")}
(CacheHint is in mcp.server.caching) for handlers that do not set the fields themselves. Choose private whenever the list varies with the
caller’s granted scopes, which the specification explicitly allows for tools/list.
Deprecated client features and what replaces them
Three client-side features were deprecated in 2026-07-28. They remain in the specification, but new implementations should not adopt them, and the earliest they can be removed is the first revision released on or after 2027-07-28.
| Feature | What it was | Use instead |
|---|---|---|
Roots |
The client told the server which directories or files it could work in. |
Pass directories and files as tool parameters or resource URIs, or configure them on the server. |
Sampling |
The server asked the client’s model for a completion. |
Integrate directly with an LLM provider API from the server (see LLM Foundations & Prompting), or restructure so the host’s agent does the reasoning and the server returns data. |
Logging ( |
The server streamed log messages to the client. |
Log to stderr on stdio servers and use OpenTelemetry for observability, with the trace context in |
Sampling and roots requests are also the request kinds that can still appear inside an input_required result during
the deprecation period; they use the same retry mechanism as elicitation. The SDKs keep supporting them (the Python
client still accepts sampling_callback and list_roots_callback, and a log_level that opts in to per-request
logging), so existing servers keep working, but a new docs-assistant-style server has no reason to use them. The
features that were removed outright — ping, the logging/setLevel request and notifications/roots/list_changed — are
covered as history on MCP Architecture.