a2a_protocol_server/handler/
input_modes.rs1use a2a_protocol_types::agent_card::AgentCard;
32use a2a_protocol_types::error::A2aError;
33use a2a_protocol_types::message::Message;
34
35use super::RequestHandler;
36use crate::error::{ServerError, ServerResult};
37
38pub fn accepted_input_modes(
40 card: Option<&AgentCard>,
41 allow_undeclared: bool,
42) -> Option<Vec<String>> {
43 if allow_undeclared {
44 return None;
45 }
46 let card = card?;
47 let mut modes: Vec<String> = card
48 .default_input_modes
49 .iter()
50 .chain(
51 card.skills
52 .iter()
53 .flat_map(|s| s.input_modes.iter().flatten()),
54 )
55 .map(|m| essence(m))
56 .filter(|m| !m.is_empty())
57 .collect();
58 modes.sort();
59 modes.dedup();
60 (!modes.is_empty()).then_some(modes)
61}
62
63fn essence(media_type: &str) -> String {
65 media_type
66 .split(';')
67 .next()
68 .unwrap_or_default()
69 .trim()
70 .to_ascii_lowercase()
71}
72
73fn admits(declared: &str, actual: &str) -> bool {
76 if declared == "*/*" || declared == actual {
77 return true;
78 }
79 declared
80 .strip_suffix("/*")
81 .is_some_and(|ty| actual.split('/').next() == Some(ty))
82}
83
84impl RequestHandler {
85 pub fn ensure_input_modes_supported(&self, message: &Message) -> ServerResult<()> {
92 let Some(accepted) = &self.accepted_input_modes else {
93 return Ok(());
94 };
95 for (i, part) in message.parts.iter().enumerate() {
96 let Some(media_type) = part.media_type.as_deref() else {
97 continue;
98 };
99 let actual = essence(media_type);
100 if !accepted.iter().any(|declared| admits(declared, &actual)) {
101 return Err(ServerError::Protocol(A2aError::content_type_not_supported(
102 format!(
103 "message part {i} has media type {media_type:?}, which the agent \
104 card does not declare (defaultInputModes or a skill's inputModes)"
105 ),
106 )));
107 }
108 }
109 Ok(())
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use a2a_protocol_types::agent_card::{
116 AgentCapabilities, AgentCard, AgentInterface, AgentSkill,
117 };
118 use a2a_protocol_types::error::ErrorCode;
119 use a2a_protocol_types::message::{Message, Part};
120
121 use crate::agent_executor;
122 use crate::builder::RequestHandlerBuilder;
123
124 struct DummyExecutor;
125 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
126
127 fn card(default: &[&str], skill: Option<&[&str]>) -> AgentCard {
128 let mut s = AgentSkill::new("s", "S", "a skill");
129 if let Some(modes) = skill {
130 s = s.with_input_modes(modes.iter().copied());
131 }
132 AgentCard {
133 url: None,
134 name: "Test Agent".into(),
135 description: "A test agent".into(),
136 version: "1.0.0".into(),
137 supported_interfaces: vec![AgentInterface {
138 url: "http://localhost:8080".into(),
139 protocol_binding: "JSONRPC".into(),
140 protocol_version: "1.0.0".into(),
141 tenant: None,
142 }],
143 default_input_modes: default.iter().map(|m| (*m).to_owned()).collect(),
144 default_output_modes: vec!["text/plain".into()],
145 skills: vec![s],
146 capabilities: AgentCapabilities::none(),
147 provider: None,
148 icon_url: None,
149 documentation_url: None,
150 security_schemes: None,
151 security_requirements: None,
152 signatures: None,
153 }
154 }
155
156 fn with_part(media_type: Option<&str>) -> Message {
157 let mut part = Part::text("hi");
158 part.media_type = media_type.map(str::to_owned);
159 Message::user("m-1", vec![part])
160 }
161
162 fn check(
164 card: Option<AgentCard>,
165 opt_out: bool,
166 media_type: Option<&str>,
167 ) -> Result<(), (ErrorCode, String)> {
168 let mut builder = RequestHandlerBuilder::new(DummyExecutor);
169 if let Some(card) = card {
170 builder = builder.with_agent_card(card);
171 }
172 if opt_out {
173 builder = builder.allow_undeclared_input_modes();
174 }
175 let handler = builder.build().expect("handler");
176 handler
177 .ensure_input_modes_supported(&with_part(media_type))
178 .map_err(|e| {
179 let e = e.to_a2a_error();
180 (e.code, e.message)
181 })
182 }
183
184 #[test]
185 fn a_declared_media_type_is_accepted() {
186 let card = card(&["application/json"], None);
187 assert!(check(Some(card), false, Some("application/json")).is_ok());
188 }
189
190 #[test]
191 fn an_undeclared_media_type_is_refused_as_content_type_not_supported() {
192 let card = card(&["text/plain"], None);
193 let (code, message) =
194 check(Some(card), false, Some("application/x-unsupported")).unwrap_err();
195 assert_eq!(code, ErrorCode::ContentTypeNotSupported);
196 assert!(message.contains("application/x-unsupported"), "{message}");
197 }
198
199 #[test]
200 fn a_mode_only_a_skill_declares_is_accepted() {
201 let card = card(&["text/plain"], Some(&["image/png"]));
202 assert!(check(Some(card), false, Some("image/png")).is_ok());
203 }
204
205 #[test]
206 fn parameters_and_case_are_ignored() {
207 let card = card(&["Text/Plain"], None);
208 assert!(check(Some(card), false, Some("text/plain; charset=utf-8")).is_ok());
209 }
210
211 #[test]
212 fn wildcards_admit_their_range_and_nothing_else() {
213 assert!(check(Some(card(&["image/*"], None)), false, Some("image/png")).is_ok());
214 assert!(check(Some(card(&["image/*"], None)), false, Some("imagex/png")).is_err());
215 assert!(check(Some(card(&["*/*"], None)), false, Some("application/x-any")).is_ok());
216 }
217
218 #[test]
219 fn a_part_without_a_media_type_is_not_checked() {
220 assert!(check(Some(card(&["application/json"], None)), false, None).is_ok());
221 }
222
223 #[test]
224 fn nothing_is_enforced_without_a_card_or_a_declaration_or_when_opted_out() {
225 assert!(check(None, false, Some("application/x-unsupported")).is_ok());
226 assert!(
227 check(
228 Some(card(&[], None)),
229 false,
230 Some("application/x-unsupported")
231 )
232 .is_ok()
233 );
234 assert!(
235 check(
236 Some(card(&["text/plain"], None)),
237 true,
238 Some("application/x-unsupported")
239 )
240 .is_ok()
241 );
242 }
243}