diff --git a/docs/protocol/draft/schema-v2.mdx b/docs/protocol/draft/schema-v2.mdx
index 155d074f..3bcba911 100644
--- a/docs/protocol/draft/schema-v2.mdx
+++ b/docs/protocol/draft/schema-v2.mdx
@@ -1903,6 +1903,10 @@ URL-based elicitation where the client directs the user to a URL.
The URL to direct the user to.
+
+ If provided, the user must visit the URL and enter this code to complete the
+ elicitation.
+
@@ -4220,6 +4224,10 @@ URL-based elicitation mode where the client directs the user to a URL.
The URL to direct the user to.
+
+ If provided, the user must visit the URL and enter this code to complete the
+ elicitation.
+
**Variants:**
diff --git a/docs/protocol/draft/schema.mdx b/docs/protocol/draft/schema.mdx
index 155d074f..3bcba911 100644
--- a/docs/protocol/draft/schema.mdx
+++ b/docs/protocol/draft/schema.mdx
@@ -1903,6 +1903,10 @@ URL-based elicitation where the client directs the user to a URL.
The URL to direct the user to.
+
+ If provided, the user must visit the URL and enter this code to complete the
+ elicitation.
+
@@ -4220,6 +4224,10 @@ URL-based elicitation mode where the client directs the user to a URL.
The URL to direct the user to.
+
+ If provided, the user must visit the URL and enter this code to complete the
+ elicitation.
+
**Variants:**
diff --git a/docs/rfds/elicitation.mdx b/docs/rfds/elicitation.mdx
index ab69c108..5fb3b23c 100644
--- a/docs/rfds/elicitation.mdx
+++ b/docs/rfds/elicitation.mdx
@@ -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`).
diff --git a/schema/schema.unstable.json b/schema/schema.unstable.json
index a76dac3b..4fc8b70f 100644
--- a/schema/schema.unstable.json
+++ b/schema/schema.unstable.json
@@ -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"],
diff --git a/schema/schema.v2.unstable.json b/schema/schema.v2.unstable.json
index a76dac3b..4fc8b70f 100644
--- a/schema/schema.v2.unstable.json
+++ b/schema/schema.v2.unstable.json
@@ -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"],
diff --git a/src/v1/elicitation.rs b/src/v1/elicitation.rs
index adc80a22..bcb8f5fb 100644
--- a/src/v1/elicitation.rs
+++ b/src/v1/elicitation.rs
@@ -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,
}
impl ElicitationUrlMode {
@@ -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) -> Self {
+ self.user_code = user_code.into_option();
+ self
+ }
}
/// **UNSTABLE**
@@ -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,
}
/// Type discriminator for URL-only elicitation error items.
@@ -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) -> Self {
+ self.user_code = user_code.into_option();
+ self
+ }
}
#[cfg(test)]
@@ -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(
@@ -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();
diff --git a/src/v2/conversion.rs b/src/v2/conversion.rs
index f1c31102..24f4cdc3 100644
--- a/src/v2/conversion.rs
+++ b/src/v2/conversion.rs
@@ -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()?,
})
}
}
@@ -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()?,
})
}
}
@@ -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()?,
})
}
}
@@ -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()?,
})
}
}
diff --git a/src/v2/elicitation.rs b/src/v2/elicitation.rs
index ba97a8b2..74defbbc 100644
--- a/src/v2/elicitation.rs
+++ b/src/v2/elicitation.rs
@@ -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,
}
impl ElicitationUrlMode {
@@ -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) -> Self {
+ self.user_code = user_code.into_option();
+ self
+ }
}
/// **UNSTABLE**
@@ -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,
}
/// Type discriminator for URL-only elicitation error items.
@@ -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) -> Self {
+ self.user_code = user_code.into_option();
+ self
+ }
}
#[cfg(test)]
@@ -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(
@@ -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();