Conversation
src/mcp/server/fastmcp/server.py
Outdated
There was a problem hiding this comment.
I think what you want would be something like this:
async def elicit(self, schema: type[T], message: str | None = None) -> T: ...And then it can be used inside the tools as:
from mcp.server.fastmcp import FastMCP
from typing_extensions import TypedDict
app = FastMCP()
class MyServerSchema(TypedDict):
name: str
@app.tool()
async def potato_tool(ctx: Context):
content = await ctx.elicit(MyServerSchema)There was a problem hiding this comment.
I added message as optional because I can imagine us extracting the message from the docstring of MyServerSchema. For the best user experience.
There was a problem hiding this comment.
Thank you! I,plemented elicit, looks nice and typesafa now!
Re message in the schema:
The message and schema serve distinct purposes in the elicitation flow:
- The message is the contextual question or prompt that explains why we're asking for information (e.g., "No tables available at 7 PM. Would you like to try 8 PM instead?")
- The schema defines what information we're collecting and its structure (e.g., fields for confirmation, alternative time, etc.)
This separation allows for:
- Dynamic context - The same schema can be reused with different messages based on the situation
- Clearer intent - The message can provide specific context that wouldn't make sense as a docstring (like "Your session will expire in 5 minutes. Save your work?")
| result = await ctx.elicit( | ||
| message=f"Confirm booking for {party_size} on {date}?", schema=ConfirmBooking | ||
| ) |
There was a problem hiding this comment.
I think it gives us a better API and user experience if ctx.elicit(schema=SchemaT) always return an instance of SchemaT, or an exception.
There was a problem hiding this comment.
My comment implies that an exception would be raised if user rejects.
There was a problem hiding this comment.
then the problem will be how to distinguish between cancel and reject?
There was a problem hiding this comment.
| result = await ctx.elicit( | |
| message=f"Confirm booking for {party_size} on {date}?", schema=ConfirmBooking | |
| ) | |
| try: | |
| result = await ctx.elicit( | |
| message=f"Confirm booking for {party_size} on {date}?", | |
| schema=ConfirmBooking | |
| ) | |
| except ElicitError as exc: | |
| print(exc.reason) |
There was a problem hiding this comment.
Wouldn't this imply that we use exceptions for control flow? We should reserve exceptions for handling exceptional circumstances. I don't think decline fits into this.
I do prefer having a return value that indicates if it was accepted,declined,etc.
| The `elicit()` method returns an `ElicitationResult` with: | ||
| - `action`: "accept", "decline", or "cancel" | ||
| - `data`: The validated response (only when accepted) | ||
| - `validation_error`: Any validation error message |
There was a problem hiding this comment.
I think this should just bubble.
dsp-ant
left a comment
There was a problem hiding this comment.
I think this PR is missing the implementation for mcp/server/lowlevel/server.py.
I decline this for now mostly because of elicitation being implement in FastMCP, but I believe it belongs at a lower level such that lowlevel server implementations have access to it.
For the tagged union appraoch, I would prefer it, but I am okay if it's not.
| result = await ctx.elicit( | ||
| message=f"Confirm booking for {party_size} on {date}?", schema=ConfirmBooking | ||
| ) |
There was a problem hiding this comment.
Wouldn't this imply that we use exceptions for control flow? We should reserve exceptions for handling exceptional circumstances. I don't think decline fits into this.
I do prefer having a return value that indicates if it was accepted,declined,etc.
src/mcp/server/fastmcp/server.py
Outdated
There was a problem hiding this comment.
Okay, I think I can be fine with this, but the FP person in me really doesn't like that this is not very typesafe, because data and validation_error depends on action.
An alternative, and something I probably prefer is using a tagged union:
from typing import Literal
class AcceptedElicitation(BaseModel, Generic[T]):
action: Literal["accept"]
data: T
class DeclinedElicitation(BaseModel):
action: Literal["decline"]
class CancelledElicitation(BaseModel):
action: Literal["cancel"]
class ValidationFailedElicitation(BaseModel):
action: Literal["accept"]
validation_error: str
ElicitationResult = AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation | ValidationFailedElicitationYou can then do:
match result:
case AcceptedElicitation(data=data):
# data is guaranteed to exist and be typed
process(data)
case DeclinedElicitation():
handle_decline()
case CancelledElicitation():
handle_cancel()
case ValidationFailedElicitation(validation_error=error):
handle_validation_error(error)There was a problem hiding this comment.
I don't think we want to have validation_error as a str, it should bubble up. User may want to handle it differently.
src/mcp/server/fastmcp/server.py
Outdated
There was a problem hiding this comment.
This should not be defined here. This should be in the ServerSession and be called from request_context.session.
The SDK is layered in such a way that FastMCP should always only contain convenience wrapping server/lowlevel or server/session, but never implement functionality itself.
0359e94 to
150f839
Compare
150f839 to
51b5ee8
Compare
Elicitation Spec PR
Overview
Adding support for the elicitation feature, allowing servers to interactively request additional information from clients during tool execution. The implementation follows a layered approach with both low-level protocol support and a high-level API through FastMCP.
Key Changes
1. Protocol Layer (
src/mcp/types.py)ElicitRequest,ElicitRequestParams, andElicitResulttypes to support the elicitation protocolServerRequestandClientResultto handle elicitation messages2. Low-Level Server/Client (
src/mcp/server/session.py,src/mcp/client/session.py)elicit()method toServerSessionfor sending elicitation requestselicit()method toClientSessionfor handling server elicitation requests3. FastMCP High-Level API (
src/mcp/server/fastmcp/server.py)Context.elicit()method that returns anElicitationResultobjectFastMCP Interface Design
The FastMCP elicitation interface prioritizes type safety and explicit result handling:
Interface characteristics: