Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/protocol/draft/schema-v2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,10 @@ URL-based elicitation where the client directs the user to a URL.
<ResponseField name="url" type={"string"} required>
The URL to direct the user to.
</ResponseField>
<ResponseField name="userCode" type={"string | null"}>
If provided, the user must visit the URL and enter this code to complete the
elicitation.
</ResponseField>

</Expandable>
</ResponseField>
Expand Down Expand Up @@ -4220,6 +4224,10 @@ URL-based elicitation mode where the client directs the user to a URL.
<ResponseField name="url" type={"string"} required>
The URL to direct the user to.
</ResponseField>
<ResponseField name="userCode" type={"string | null"}>
If provided, the user must visit the URL and enter this code to complete the
elicitation.
</ResponseField>

**Variants:**

Expand Down
8 changes: 8 additions & 0 deletions docs/protocol/draft/schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,10 @@ URL-based elicitation where the client directs the user to a URL.
<ResponseField name="url" type={"string"} required>
The URL to direct the user to.
</ResponseField>
<ResponseField name="userCode" type={"string | null"}>
If provided, the user must visit the URL and enter this code to complete the
elicitation.
</ResponseField>

</Expandable>
</ResponseField>
Expand Down Expand Up @@ -4220,6 +4224,10 @@ URL-based elicitation mode where the client directs the user to a URL.
<ResponseField name="url" type={"string"} required>
The URL to direct the user to.
</ResponseField>
<ResponseField name="userCode" type={"string | null"}>
If provided, the user must visit the URL and enter this code to complete the
elicitation.
</ResponseField>

**Variants:**

Expand Down
2 changes: 2 additions & 0 deletions docs/rfds/elicitation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ Following MCP's approach (specifically [SEP-1036](https://modelcontextprotocol.i

This distinction is reflected in the client capabilities model, allowing clients to declare support for one or both modalities.

URL mode supports an optional `userCode` parameter. If present, the user must enter this code when they visit the external URL.

**Normative requirements:**

- Clients declaring the `elicitation` capability MUST support at least one mode (`form` or `url`).
Expand Down
4 changes: 4 additions & 0 deletions schema/schema.unstable.json
Original file line number Diff line number Diff line change
Expand Up @@ -2713,6 +2713,10 @@
"description": "The URL to direct the user to.",
"format": "uri",
"type": "string"
},
"userCode": {
"description": "If provided, the user must visit the URL and enter this code to complete the elicitation.",
"type": ["string", "null"]
}
},
"required": ["elicitationId", "url"],
Expand Down
4 changes: 4 additions & 0 deletions schema/schema.v2.unstable.json
Original file line number Diff line number Diff line change
Expand Up @@ -2713,6 +2713,10 @@
"description": "The URL to direct the user to.",
"format": "uri",
"type": "string"
},
"userCode": {
"description": "If provided, the user must visit the URL and enter this code to complete the elicitation.",
"type": ["string", "null"]
}
},
"required": ["elicitationId", "url"],
Expand Down
76 changes: 76 additions & 0 deletions src/v1/elicitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,8 @@ pub struct ElicitationUrlMode {
/// The URL to direct the user to.
#[schemars(extend("format" = "uri"))]
pub url: String,
/// If provided, the user must visit the URL and enter this code to complete the elicitation.
pub user_code: Option<String>,
}

impl ElicitationUrlMode {
Expand All @@ -1088,8 +1090,16 @@ impl ElicitationUrlMode {
scope: scope.into(),
elicitation_id: elicitation_id.into(),
url: url.into(),
user_code: None,
}
}

/// If provided, the user must visit the URL and enter this code to complete the elicitation.
#[must_use]
pub fn user_code(mut self, user_code: impl IntoOption<String>) -> Self {
self.user_code = user_code.into_option();
self
}
}

/// **UNSTABLE**
Expand Down Expand Up @@ -1330,6 +1340,8 @@ pub struct UrlElicitationRequiredItem {
pub url: String,
/// A human-readable message describing what input is needed.
pub message: String,
/// If provided, the user must visit the URL and enter this code to complete the elicitation.
pub user_code: Option<String>,
}

/// Type discriminator for URL-only elicitation error items.
Expand All @@ -1354,8 +1366,16 @@ impl UrlElicitationRequiredItem {
elicitation_id: elicitation_id.into(),
url: url.into(),
message: message.into(),
user_code: None,
}
}

/// If provided, the user must visit the URL and enter this code to complete the elicitation.
#[must_use]
pub fn user_code(mut self, user_code: impl IntoOption<String>) -> Self {
self.user_code = user_code.into_option();
self
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1421,6 +1441,37 @@ mod tests {
assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
}

#[test]
fn url_mode_request_with_user_code_serialization() {
let req = CreateElicitationRequest::new(
ElicitationUrlMode::new(
ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
"elic_1",
"https://example.com/auth",
)
.user_code("ABCDEF-123456"),
"Please authenticate",
);

let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["sessionId"], "sess_2");
assert_eq!(json["toolCallId"], "tc_1");
assert_eq!(json["mode"], "url");
assert_eq!(json["elicitationId"], "elic_1");
assert_eq!(json["url"], "https://example.com/auth");
assert_eq!(json["message"], "Please authenticate");
assert_eq!(json["userCode"], "ABCDEF-123456");

let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
assert_eq!(
*roundtripped.scope(),
ElicitationSessionScope::new("sess_2")
.tool_call_id("tc_1")
.into()
);
assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
}

#[test]
fn response_accept_serialization() {
let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
Expand Down Expand Up @@ -1665,6 +1716,31 @@ mod tests {
);
}

#[test]
fn url_elicitation_required_with_user_code_data_serialization() {
let data = UrlElicitationRequiredData::new(vec![
UrlElicitationRequiredItem::new(
"elic_1",
"https://example.com/auth",
"Please authenticate",
)
.user_code("ABCDE-12345"),
]);

let json = serde_json::to_value(&data).unwrap();
assert_eq!(json["elicitations"][0]["mode"], "url");
assert_eq!(json["elicitations"][0]["elicitationId"], "elic_1");
assert_eq!(json["elicitations"][0]["url"], "https://example.com/auth");
assert_eq!(json["elicitations"][0]["userCode"], "ABCDE-12345");

let roundtripped: UrlElicitationRequiredData = serde_json::from_value(json).unwrap();
assert_eq!(roundtripped.elicitations.len(), 1);
assert_eq!(
roundtripped.elicitations[0].mode,
ElicitationUrlOnlyMode::Url
);
}

#[test]
fn schema_default_sets_object_type() {
let schema = ElicitationSchema::default();
Expand Down
8 changes: 8 additions & 0 deletions src/v2/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8587,11 +8587,13 @@ impl IntoV1 for super::ElicitationUrlMode {
scope,
elicitation_id,
url,
user_code,
} = self;
Ok(crate::v1::ElicitationUrlMode {
scope: scope.into_v1()?,
elicitation_id: elicitation_id.into_v1()?,
url: url.into_v1()?,
user_code: user_code.into_v1()?,
})
}
}
Expand All @@ -8605,11 +8607,13 @@ impl IntoV2 for crate::v1::ElicitationUrlMode {
scope,
elicitation_id,
url,
user_code,
} = self;
Ok(super::ElicitationUrlMode {
scope: scope.into_v2()?,
elicitation_id: elicitation_id.into_v2()?,
url: url.into_v2()?,
user_code: user_code.into_v2()?,
})
}
}
Expand Down Expand Up @@ -8790,12 +8794,14 @@ impl IntoV1 for super::UrlElicitationRequiredItem {
elicitation_id,
url,
message,
user_code,
} = self;
Ok(crate::v1::UrlElicitationRequiredItem {
mode: mode.into_v1()?,
elicitation_id: elicitation_id.into_v1()?,
url: url.into_v1()?,
message: message.into_v1()?,
user_code: user_code.into_v1()?,
})
}
}
Expand All @@ -8810,12 +8816,14 @@ impl IntoV2 for crate::v1::UrlElicitationRequiredItem {
elicitation_id,
url,
message,
user_code,
} = self;
Ok(super::UrlElicitationRequiredItem {
mode: mode.into_v2()?,
elicitation_id: elicitation_id.into_v2()?,
url: url.into_v2()?,
message: message.into_v2()?,
user_code: user_code.into_v2()?,
})
}
}
Expand Down
76 changes: 76 additions & 0 deletions src/v2/elicitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,8 @@ pub struct ElicitationUrlMode {
/// The URL to direct the user to.
#[schemars(extend("format" = "uri"))]
pub url: String,
/// If provided, the user must visit the URL and enter this code to complete the elicitation.
pub user_code: Option<String>,
}

impl ElicitationUrlMode {
Expand All @@ -1089,8 +1091,16 @@ impl ElicitationUrlMode {
scope: scope.into(),
elicitation_id: elicitation_id.into(),
url: url.into(),
user_code: None,
}
}

/// If provided, the user must visit the URL and enter this code to complete the elicitation.
#[must_use]
pub fn user_code(mut self, user_code: impl IntoOption<String>) -> Self {
self.user_code = user_code.into_option();
self
}
}

/// **UNSTABLE**
Expand Down Expand Up @@ -1331,6 +1341,8 @@ pub struct UrlElicitationRequiredItem {
pub url: String,
/// A human-readable message describing what input is needed.
pub message: String,
/// If provided, the user must visit the URL and enter this code to complete the elicitation.
pub user_code: Option<String>,
}

/// Type discriminator for URL-only elicitation error items.
Expand All @@ -1355,8 +1367,16 @@ impl UrlElicitationRequiredItem {
elicitation_id: elicitation_id.into(),
url: url.into(),
message: message.into(),
user_code: None,
}
}

/// If provided, the user must visit the URL and enter this code to complete the elicitation.
#[must_use]
pub fn user_code(mut self, user_code: impl IntoOption<String>) -> Self {
self.user_code = user_code.into_option();
self
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1422,6 +1442,37 @@ mod tests {
assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
}

#[test]
fn url_mode_request_with_user_code_serialization() {
let req = CreateElicitationRequest::new(
ElicitationUrlMode::new(
ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
"elic_1",
"https://example.com/auth",
)
.user_code("ABCDEF-123456"),
"Please authenticate",
);

let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["sessionId"], "sess_2");
assert_eq!(json["toolCallId"], "tc_1");
assert_eq!(json["mode"], "url");
assert_eq!(json["elicitationId"], "elic_1");
assert_eq!(json["url"], "https://example.com/auth");
assert_eq!(json["message"], "Please authenticate");
assert_eq!(json["userCode"], "ABCDEF-123456");

let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
assert_eq!(
*roundtripped.scope(),
ElicitationSessionScope::new("sess_2")
.tool_call_id("tc_1")
.into()
);
assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
}

#[test]
fn response_accept_serialization() {
let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
Expand Down Expand Up @@ -1666,6 +1717,31 @@ mod tests {
);
}

#[test]
fn url_elicitation_required_with_user_code_data_serialization() {
let data = UrlElicitationRequiredData::new(vec![
UrlElicitationRequiredItem::new(
"elic_1",
"https://example.com/auth",
"Please authenticate",
)
.user_code("ABCDE-12345"),
]);

let json = serde_json::to_value(&data).unwrap();
assert_eq!(json["elicitations"][0]["mode"], "url");
assert_eq!(json["elicitations"][0]["elicitationId"], "elic_1");
assert_eq!(json["elicitations"][0]["url"], "https://example.com/auth");
assert_eq!(json["elicitations"][0]["userCode"], "ABCDE-12345");

let roundtripped: UrlElicitationRequiredData = serde_json::from_value(json).unwrap();
assert_eq!(roundtripped.elicitations.len(), 1);
assert_eq!(
roundtripped.elicitations[0].mode,
ElicitationUrlOnlyMode::Url
);
}

#[test]
fn schema_default_sets_object_type() {
let schema = ElicitationSchema::default();
Expand Down