The server asks the LLM. The server asks the user. Both need consent.
MCP Servers Module 3
Client asks server: tools, resources, prompts. Every read or call flows one direction.
The server asks back — either the LLM (sampling) or the user (elicitation).
This is where MCP stops being a one-way API.
A tool can request the client's LLM to generate a completion mid-execution. The server doesn't hold the API key — the client does. The server says "summarize this", the client routes the request through its own LLM, and returns the result.
Cost lives with the user, not the server operator. Their tokens, their consent.
from mcp.server.fastmcp import Context
from mcp.types import SamplingMessage, TextContent
@mcp.tool()
async def summarize(id: str, ctx: Context) -> str:
"""Summarize a note via the client's LLM."""
body = NOTES[id]
result = await ctx.session.create_message(
messages=[SamplingMessage(
role="user",
content=TextContent(type="text", text=f"Summarize in 2 sentences:\n\n{body}"),
)],
max_tokens=200,
)
return result.content.text
ctx.session.create_message is the server-initiated LLM call.
Always:
max_tokens.A tool can request structured input from the user mid-execution. Example: pkb.write_note mid-call asks "what title?" if the LLM didn't pass one.
The client surfaces a form; the user fills it; the value comes back to the server. Nothing is guessed and nothing is silently defaulted.
from mcp.types import ElicitationSchema
result = await ctx.session.elicit(
message="Pick a title for the new note:",
requested_schema={
"type": "object",
"properties": {"title": {"type": "string"}},
"required": ["title"],
},
)
title = result.content["title"] if result.action == "accept" else None
The client owns the UX; the server only declares the shape it needs.
You want the LLM's synthesis — summarize, classify, rephrase, extract.
You want the user's clarification — pick a folder, confirm an action, supply a missing arg.
Never use sampling to ask the user for input — that's smuggling.
pkb.summarize(id) tool that uses ctx.session.create_message to summarize a note via sampling.ctx: Context parameter → no access to the session..content.text, not .text.async def tool — create_message is async.summarize into a real multi-tool flow with write and composition.