Skip to content

Commit c7560df

Browse files
committed
Address PR review: align URL elicitation with spec
Remove non-spec types and methods per reviewer feedback: - Remove ElicitTrackRequest, ElicitTrackRequestParams, ElicitTrackResult - Remove track_elicitation() method from ClientSession - Revert CONNECTION_CLOSED to -32000 (no conflict with -32042) Align type structure with spec's discriminated union: - Create separate ElicitRequestFormParams and ElicitRequestURLParams - Make ElicitRequestParams a TypeAlias for the union - Form mode now requires requestedSchema (per spec) - URL mode requires url and elicitationId (per spec) Fix ElicitResult.content type to match spec: - Add list[str] support for array values - Remove float and None (not in spec) Consolidate error types: - Remove redundant UrlElicitationInfo - Use ElicitRequestURLParams in ElicitationRequiredErrorData Update server session to use new typed params classes.
1 parent cbfccec commit c7560df

File tree

6 files changed

+61
-124
lines changed

6 files changed

+61
-124
lines changed

src/mcp/client/session.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -509,29 +509,6 @@ async def list_tools(
509509

510510
return result
511511

512-
async def track_elicitation(
513-
self,
514-
elicitation_id: str,
515-
progress_token: types.ProgressToken | None = None,
516-
) -> types.ElicitTrackResult:
517-
"""Send an elicitation/track request to monitor URL mode elicitation progress.
518-
519-
Args:
520-
elicitation_id: The unique identifier of the elicitation to track
521-
progress_token: Optional token for receiving progress notifications
522-
523-
Returns:
524-
ElicitTrackResult indicating the status of the elicitation
525-
"""
526-
params = types.ElicitTrackRequestParams(elicitationId=elicitation_id) # pragma: no cover
527-
if progress_token is not None: # pragma: no cover
528-
params.meta = types.RequestParams.Meta(progressToken=progress_token)
529-
530-
return await self.send_request( # pragma: no cover
531-
types.ClientRequest(types.ElicitTrackRequest(params=params)),
532-
types.ElicitTrackResult,
533-
)
534-
535512
async def send_roots_list_changed(self) -> None: # pragma: no cover
536513
"""Send a roots/list_changed notification."""
537514
await self.send_notification(types.ClientNotification(types.RootsListChangedNotification()))

src/mcp/server/elicitation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ async def elicit_url(
139139
- Any interaction where data should not pass through the LLM context
140140
141141
The response indicates whether the user consented to navigate to the URL.
142-
The actual interaction happens out-of-band, and you can track progress using
143-
session.track_elicitation().
142+
The actual interaction happens out-of-band. When the elicitation completes,
143+
the server should send an ElicitCompleteNotification to notify the client.
144144
145145
Args:
146146
session: The server session

src/mcp/server/session.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,7 @@ async def elicit_form(
370370
return await self.send_request(
371371
types.ServerRequest(
372372
types.ElicitRequest(
373-
params=types.ElicitRequestParams(
374-
mode="form",
373+
params=types.ElicitRequestFormParams(
375374
message=message,
376375
requestedSchema=requestedSchema,
377376
),
@@ -405,8 +404,7 @@ async def elicit_url(
405404
return await self.send_request(
406405
types.ServerRequest(
407406
types.ElicitRequest(
408-
params=types.ElicitRequestParams(
409-
mode="url",
407+
params=types.ElicitRequestURLParams(
410408
message=message,
411409
url=url,
412410
elicitationId=elicitation_id,

src/mcp/types.py

Lines changed: 48 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ class JSONRPCResponse(BaseModel):
151151
"""Error code indicating that a URL mode elicitation is required before the request can be processed."""
152152

153153
# SDK error codes
154-
CONNECTION_CLOSED = -32001
155-
# REQUEST_TIMEOUT = -32002 # the typescript sdk uses this
154+
CONNECTION_CLOSED = -32000
155+
# REQUEST_TIMEOUT = -32001 # the typescript sdk uses this
156156

157157
# Standard JSON-RPC error codes
158158
PARSE_ERROR = -32700
@@ -1462,22 +1462,6 @@ class ElicitCompleteNotification(
14621462
params: ElicitCompleteNotificationParams
14631463

14641464

1465-
class ElicitTrackRequestParams(RequestParams):
1466-
"""Parameters for elicitation tracking requests."""
1467-
1468-
elicitationId: str
1469-
"""The unique identifier of the elicitation to track."""
1470-
1471-
model_config = ConfigDict(extra="allow")
1472-
1473-
1474-
class ElicitTrackRequest(Request[ElicitTrackRequestParams, Literal["elicitation/track"]]):
1475-
"""A request from the client to track progress of a URL mode elicitation."""
1476-
1477-
method: Literal["elicitation/track"] = "elicitation/track"
1478-
params: ElicitTrackRequestParams
1479-
1480-
14811465
class ClientRequest(
14821466
RootModel[
14831467
PingRequest
@@ -1493,7 +1477,6 @@ class ClientRequest(
14931477
| UnsubscribeRequest
14941478
| CallToolRequest
14951479
| ListToolsRequest
1496-
| ElicitTrackRequest
14971480
]
14981481
):
14991482
pass
@@ -1510,34 +1493,58 @@ class ClientNotification(
15101493
"""Schema for elicitation requests."""
15111494

15121495

1513-
class ElicitRequestParams(RequestParams):
1514-
"""Parameters for elicitation requests.
1496+
class ElicitRequestFormParams(RequestParams):
1497+
"""Parameters for form mode elicitation requests.
15151498
1516-
The mode field determines the type of elicitation:
1517-
- "form": In-band structured data collection with optional schema validation
1518-
- "url": Out-of-band interaction via URL navigation
1499+
Form mode collects non-sensitive information from the user via an in-band form
1500+
rendered by the client.
15191501
"""
15201502

1521-
mode: Literal["form", "url"]
1522-
"""The mode of elicitation (form or url)."""
1503+
mode: Literal["form"] = "form"
1504+
"""The elicitation mode (always "form" for this type)."""
15231505

15241506
message: str
1525-
"""A human-readable message explaining why the interaction is needed."""
1507+
"""The message to present to the user describing what information is being requested."""
1508+
1509+
requestedSchema: ElicitRequestedSchema
1510+
"""
1511+
A restricted subset of JSON Schema defining the structure of expected response.
1512+
Only top-level properties are allowed, without nesting.
1513+
"""
15261514

1527-
# Form mode fields
1528-
requestedSchema: ElicitRequestedSchema | None = None
1529-
"""JSON Schema defining the structure of expected response (form mode only)."""
1515+
model_config = ConfigDict(extra="allow")
1516+
1517+
1518+
class ElicitRequestURLParams(RequestParams):
1519+
"""Parameters for URL mode elicitation requests.
1520+
1521+
URL mode directs users to external URLs for sensitive out-of-band interactions
1522+
like OAuth flows, credential collection, or payment processing.
1523+
"""
1524+
1525+
mode: Literal["url"] = "url"
1526+
"""The elicitation mode (always "url" for this type)."""
1527+
1528+
message: str
1529+
"""The message to present to the user explaining why the interaction is needed."""
15301530

1531-
# URL mode fields
1532-
url: str | None = None
1533-
"""The URL that the user should navigate to (url mode only)."""
1531+
url: str
1532+
"""The URL that the user should navigate to."""
15341533

1535-
elicitationId: str | None = None
1536-
"""A unique identifier for the elicitation (url mode only)."""
1534+
elicitationId: str
1535+
"""
1536+
The ID of the elicitation, which must be unique within the context of the server.
1537+
The client MUST treat this ID as an opaque value.
1538+
"""
15371539

15381540
model_config = ConfigDict(extra="allow")
15391541

15401542

1543+
# Union type for elicitation request parameters
1544+
ElicitRequestParams: TypeAlias = ElicitRequestURLParams | ElicitRequestFormParams
1545+
"""Parameters for elicitation requests - either form or URL mode."""
1546+
1547+
15411548
class ElicitRequest(Request[ElicitRequestParams, Literal["elicitation/create"]]):
15421549
"""A request from the server to elicit information from the client."""
15431550

@@ -1556,59 +1563,29 @@ class ElicitResult(Result):
15561563
- "cancel": User dismissed without making an explicit choice
15571564
"""
15581565

1559-
content: dict[str, str | int | float | bool | None] | None = None
1566+
content: dict[str, str | int | bool | list[str]] | None = None
15601567
"""
15611568
The submitted form data, only present when action is "accept" in form mode.
1562-
Contains values matching the requested schema.
1569+
Contains values matching the requested schema. Values can be strings, integers,
1570+
booleans, or arrays of strings.
15631571
For URL mode, this field is omitted.
15641572
"""
15651573

15661574

1567-
class ElicitTrackResult(Result):
1568-
"""The server's response to an elicitation tracking request."""
1569-
1570-
status: Literal["pending", "complete"]
1571-
"""
1572-
The status of the elicitation.
1573-
- "pending": The elicitation is still in progress
1574-
- "complete": The elicitation has been completed
1575-
"""
1576-
1577-
model_config = ConfigDict(extra="allow")
1578-
1579-
1580-
class UrlElicitationInfo(BaseModel):
1581-
"""Information about a URL mode elicitation embedded in an ElicitationRequired error."""
1582-
1583-
mode: Literal["url"] = "url"
1584-
"""The mode of elicitation (must be "url")."""
1585-
1586-
elicitationId: str
1587-
"""A unique identifier for the elicitation."""
1588-
1589-
url: str
1590-
"""The URL that the user should navigate to."""
1591-
1592-
message: str
1593-
"""A human-readable message explaining why the interaction is needed."""
1594-
1595-
model_config = ConfigDict(extra="allow")
1596-
1597-
15981575
class ElicitationRequiredErrorData(BaseModel):
1599-
"""Error data for ElicitationRequired errors.
1576+
"""Error data for URLElicitationRequiredError.
16001577
16011578
Servers return this when a request cannot be processed until one or more
16021579
URL mode elicitations are completed.
16031580
"""
16041581

1605-
elicitations: list[UrlElicitationInfo]
1582+
elicitations: list[ElicitRequestURLParams]
16061583
"""List of URL mode elicitations that must be completed."""
16071584

16081585
model_config = ConfigDict(extra="allow")
16091586

16101587

1611-
class ClientResult(RootModel[EmptyResult | CreateMessageResult | ListRootsResult | ElicitResult | ElicitTrackResult]):
1588+
class ClientResult(RootModel[EmptyResult | CreateMessageResult | ListRootsResult | ElicitResult]):
16121589
pass
16131590

16141591

tests/server/fastmcp/test_elicitation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88
from pydantic import BaseModel, Field
99

10+
from mcp import types
1011
from mcp.client.session import ClientSession, ElicitationFnT
1112
from mcp.server.fastmcp import Context, FastMCP
1213
from mcp.server.session import ServerSession
@@ -247,8 +248,8 @@ async def defaults_tool(ctx: Context[ServerSession, None]) -> str:
247248
# First verify that defaults are present in the JSON schema sent to clients
248249
async def callback_schema_verify(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
249250
# Verify the schema includes defaults
251+
assert isinstance(params, types.ElicitRequestFormParams), "Expected form mode elicitation"
250252
schema = params.requestedSchema
251-
assert schema is not None, "Schema should not be None for form mode elicitation"
252253
props = schema["properties"]
253254

254255
assert props["name"]["default"] == "Guest"

tests/server/fastmcp/test_url_elicitation.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,12 @@ async def check_url_response(ctx: Context[ServerSession, None]) -> str:
157157
return f"Action: {result.action}, Content: {result.content}"
158158

159159
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
160-
# Verify that no content field is expected for URL mode
160+
# Verify that this is URL mode
161161
assert params.mode == "url"
162-
assert params.requestedSchema is None
162+
assert isinstance(params, types.ElicitRequestURLParams)
163+
# URL params have url and elicitationId, not requestedSchema
164+
assert params.url == "https://example.com/test"
165+
assert params.elicitationId == "test-001"
163166
# Return without content - this is correct for URL mode
164167
return ElicitResult(action="accept")
165168

@@ -195,9 +198,9 @@ async def ask_name(ctx: Context[ServerSession, None]) -> str:
195198
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
196199
# Verify form mode parameters
197200
assert params.mode == "form"
201+
assert isinstance(params, types.ElicitRequestFormParams)
202+
# Form params have requestedSchema, not url/elicitationId
198203
assert params.requestedSchema is not None
199-
assert params.url is None
200-
assert params.elicitationId is None
201204
return ElicitResult(action="accept", content={"name": "Alice"})
202205

203206
async with create_connected_server_and_client_session(
@@ -261,25 +264,6 @@ async def test_url_elicitation_required_error_code():
261264
)
262265

263266

264-
@pytest.mark.anyio
265-
async def test_track_elicitation_method_exists():
266-
"""Test that track_elicitation method exists on ClientSession."""
267-
# This test just verifies the method signature and parameter handling exist
268-
# without actually calling the server (which may not implement it yet)
269-
import inspect
270-
271-
from mcp.client.session import ClientSession
272-
273-
# Verify the method exists
274-
assert hasattr(ClientSession, "track_elicitation")
275-
276-
# Verify the method signature
277-
sig = inspect.signature(ClientSession.track_elicitation)
278-
params = list(sig.parameters.keys())
279-
assert "elicitation_id" in params
280-
assert "progress_token" in params
281-
282-
283267
@pytest.mark.anyio
284268
async def test_elicit_url_typed_results():
285269
"""Test that elicit_url returns properly typed result objects."""

0 commit comments

Comments
 (0)