You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
According to the Model Context Protocol (MCP) specification (Sampling), the MCP Server can forward prompts to the client for execution at a large language model (LLM) through a sampling mechanism.
I am trying to implement a use case where the MCP Server performs "sampling" and requests the MCP Client to execute a prompt and return the output without involving a User-In-The-Loop.
Essentially, I want the MCP Server to trigger prompt execution independently and receive the result from the client, and that the MCP Server can trigger prompt execution whenever the server needs it.
I have some questions to clarify this setup:
Is it correct that the MCP Server can independently initiate a sampling message to the MCP Client, without the MCP Client first invoking a TOOL (or something else) on the MCP Server?
If this is possible, could you please provide example code or guidance demonstrating how to implement this flow where the MCP Server triggers sampling and the client responds with the prompt output?
Yes the MCP server can initiate a sampling request whenever it wants without the client having first sent a tool call. Although, this does require a stateful connection to be open, whether that be the now deprecated SSE transport, or Streamable HTTP in stateful mode.
@maxisbey okay understood. I need to use a stateful connection like Streamable HTTP. Before, I was trying to implement this Use Case with STDIO...
Regarding to the second bulletpoint: This only covers a MCP Client which enables sampling capability, as far I understood. Thats only one piece.
The other interesting part would be how a MCP Server sends a sampling message to the client. How can I implement this? Any hints which methods from the SDK shall I use for this?
Hi @Gerr1ck , I'm not familiar with any official docs but if a dummy sample is enough for you to get a first look (this is what I tested the PR client initially with), you can take a look here.
There is no explicit call to parse_arithmetic_expression. It's an MCP tool, and converted and exposed that way here. I could have used .tool decorator as well, would have worked similarly.
It never needs to be called explicitly, based on the available tools and contracts, LLM decides what to call with what argument and then MCP client calls the same. You can use my repo above for client example, or the PR I created also have a minimal version.
I'm not exactly familiar with context creation, I believe it's set at low level, and it's part of the contract if you want to use sampling/elicitation. But hopefully @maxisbey or @felixweinberger can answer in details more, I'm not too familiar at the low level details.
Imagine that the MCP Server just wants to call [sampling] itself. From where does the MCP Server get the context?
There are two scenarios here:
1. Sampling from inside a tool (the common case)
The Context is injected automatically — you never construct it yourself. Just add a Context-typed parameter to your tool and the framework fills it in when the client calls the tool. Then use ctx.session.create_message():
Server — FastMCP (v1.x) / MCPServer (main)
frommcp.server.fastmcpimportContext, FastMCP# on main: from mcp.server.mcpserver import Context, MCPServerfrommcp.typesimportSamplingMessage, TextContentmcp=FastMCP("sampling-demo")
@mcp.tool()asyncdefsummarize(text: str, ctx: Context) ->str:
"""Ask the client's LLM to summarize some text."""result=awaitctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=f"Summarize: {text}"),
)
],
max_tokens=100,
)
returnresult.content.textifresult.content.type=="text"elsestr(result.content)
if__name__=="__main__":
mcp.run()
Client
importanyiofrommcp.client.sessionimportClientSessionfrommcp.client.stdioimportStdioServerParameters, stdio_clientfrommcp.typesimportCreateMessageResult, TextContentasyncdefhandle_sampling(context, params) ->CreateMessageResult:
# In a real client you'd call your LLM herereturnCreateMessageResult(
role="assistant",
content=TextContent(type="text", text="A brief summary."),
model="mock-model",
stopReason="endTurn", # stop_reason= on main
)
asyncdefmain():
params=StdioServerParameters(command="python", args=["server.py"])
asyncwithstdio_client(params) as (read, write):
asyncwithClientSession(read, write, sampling_callback=handle_sampling) assession:
awaitsession.initialize()
result=awaitsession.call_tool("summarize", {"text": "long document..."})
print(result.content[0].text)
anyio.run(main)
2. Server-initiated sampling (no tool call)
If you want the server to sample on its own schedule without the client calling a tool first, Context won't help — it only exists inside request handlers. You need to drop to the low-level ServerSession directly.
The catch: server.run() creates the ServerSession internally and doesn't expose it. The workaround is to skip server.run(), create the session yourself, and run the message loop alongside your own background task.
This works over stdio (it's a stateful connection, no need for HTTP):
Server
importsysimportanyiofrommcp.serverimportServerfrommcp.server.sessionimportInitializationState, ServerSessionfrommcp.server.stdioimportstdio_serverfrommcp.typesimportSamplingMessage, TextContentserver=Server("sampling-demo")
asyncdefbackground_work(session: ServerSession) ->None:
# Wait for the client to finish the initialize handshakewhilesession._initialization_state!=InitializationState.Initialized:
awaitanyio.sleep(0.05)
# Now the server can sample whenever it wants — no tool call neededforiinrange(3):
result=awaitsession.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=f"Say hello #{i}"),
)
],
max_tokens=100,
)
print(f"[server] got: {result.content.text}", file=sys.stderr)
awaitanyio.sleep(0.5)
asyncdefmain() ->None:
asyncwithstdio_server() as (read, write):
# Create the session ourselves instead of calling server.run()asyncwithServerSession(read, write, server.create_initialization_options()) assession:
asyncwithanyio.create_task_group() astg:
tg.start_soon(background_work, session)
# The normal message-processing loopasyncformsginsession.incoming_messages:
tg.start_soon(server._handle_message, msg, session, {}, False)
anyio.run(main)
Client
importanyiofrommcp.client.sessionimportClientSessionfrommcp.client.stdioimportStdioServerParameters, stdio_clientfrommcp.typesimportCreateMessageRequestParams, CreateMessageResult, TextContentasyncdefhandle_sampling(context, params: CreateMessageRequestParams) ->CreateMessageResult:
user_text=params.messages[0].content.textprint(f"[client] server asked: {user_text!r}")
# In a real client you'd call your LLM herereturnCreateMessageResult(
role="assistant",
content=TextContent(type="text", text=f"(mock reply to: {user_text})"),
model="mock-model",
stopReason="endTurn", # stop_reason= on main
)
asyncdefmain() ->None:
params=StdioServerParameters(command="python", args=["server.py"])
asyncwithstdio_client(params) as (read, write):
asyncwithClientSession(read, write, sampling_callback=handle_sampling) assession:
awaitsession.initialize()
# Keep the connection open so the server's background task can sampleawaitanyio.sleep(3)
anyio.run(main)
Output
$ python client.py
[client] server asked: 'Say hello #0'
[server] got: (mock reply to: Say hello #0)
[client] server asked: 'Say hello #1'
[server] got: (mock reply to: Say hello #1)
[client] server asked: 'Say hello #2'
[server] got: (mock reply to: Say hello #2)
Notes
Approach 2 pokes at two private-ish members (_initialization_state and _handle_message). There isn't currently a cleaner public hook for "give me the session once a client connects" — if that's something you'd find useful as a first-class API, feel free to open a feature request.
Both approaches work identically on v1.x and main. The only differences are import paths (FastMCP → MCPServer) and field naming (stopReason → stop_reason).
For Streamable HTTP in stateful mode the idea is the same, but you'd capture the session from StreamableHTTPSessionManager._server_instances instead (one session per connected client).
Closing this for now — feel free to reopen if you have further questions.
Activity
maxisbey commentedon Nov 4, 2025
Gerr1ck commentedon Nov 4, 2025
@maxisbey okay understood. I need to use a stateful connection like Streamable HTTP. Before, I was trying to implement this Use Case with STDIO...
Regarding to the second bulletpoint: This only covers a MCP Client which enables sampling capability, as far I understood. Thats only one piece.
The other interesting part would be how a MCP Server sends a sampling message to the client. How can I implement this? Any hints which methods from the SDK shall I use for this?
yarnabrina commentedon Nov 4, 2025
Hi @Gerr1ck , I'm not familiar with any official docs but if a dummy sample is enough for you to get a first look (this is what I tested the PR client initially with), you can take a look here.
https://github.com/yarnabrina/learn-model-context-protocol/blob/main/src/mcp_learning/mcp_server/simplification.py#L13-L66
(This is not stateful though.)
Gerr1ck commentedon Nov 5, 2025
Hi @yarnabrina thank you the link.
Question to that:
I cannot find a reference where the method
async def parse_arithmetic_expression(text: str, context: Context) -> str:gets called.
Can you explain me how this method gets called and which
contextshall be used?Imagine that the MCP Server just want to call
parse_arithmetic_expressionitself. From where do the MCP Server gets thecontext?yarnabrina commentedon Nov 5, 2025
There is no explicit call to
parse_arithmetic_expression. It's an MCP tool, and converted and exposed that way here. I could have used.tooldecorator as well, would have worked similarly.It never needs to be called explicitly, based on the available tools and contracts, LLM decides what to call with what argument and then MCP client calls the same. You can use my repo above for client example, or the PR I created also have a minimal version.
I'm not exactly familiar with
contextcreation, I believe it's set at low level, and it's part of the contract if you want to use sampling/elicitation. But hopefully @maxisbey or @felixweinberger can answer in details more, I'm not too familiar at the low level details.maxisbey commentedon Mar 24, 2026
Hi @Gerr1ck, to answer your follow-up directly:
There are two scenarios here:
1. Sampling from inside a tool (the common case)
The
Contextis injected automatically — you never construct it yourself. Just add aContext-typed parameter to your tool and the framework fills it in when the client calls the tool. Then usectx.session.create_message():Server —
FastMCP(v1.x) /MCPServer(main)Client
2. Server-initiated sampling (no tool call)
If you want the server to sample on its own schedule without the client calling a tool first,
Contextwon't help — it only exists inside request handlers. You need to drop to the low-levelServerSessiondirectly.The catch:
server.run()creates theServerSessioninternally and doesn't expose it. The workaround is to skipserver.run(), create the session yourself, and run the message loop alongside your own background task.This works over stdio (it's a stateful connection, no need for HTTP):
Server
Client
Output
Notes
_initialization_stateand_handle_message). There isn't currently a cleaner public hook for "give me the session once a client connects" — if that's something you'd find useful as a first-class API, feel free to open a feature request.v1.xandmain. The only differences are import paths (FastMCP→MCPServer) and field naming (stopReason→stop_reason).StreamableHTTPSessionManager._server_instancesinstead (one session per connected client).Closing this for now — feel free to reopen if you have further questions.
AI Disclaimer