1use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[cfg(not(feature = "std"))]
13use alloc::{
14 collections::BTreeMap as HashMap,
15 string::{String, ToString},
16 vec::Vec,
17};
18#[cfg(feature = "std")]
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
23pub struct ServerInfo {
24 pub name: String,
26 pub version: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub title: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub description: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub icons: Option<Vec<Icon>>,
37 #[serde(rename = "websiteUrl", skip_serializing_if = "Option::is_none")]
39 pub website_url: Option<String>,
40}
41
42impl ServerInfo {
43 #[must_use]
45 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
46 Self {
47 name: name.into(),
48 version: version.into(),
49 ..Default::default()
50 }
51 }
52
53 #[must_use]
55 pub fn with_title(mut self, title: impl Into<String>) -> Self {
56 self.title = Some(title.into());
57 self
58 }
59
60 #[must_use]
62 pub fn with_description(mut self, description: impl Into<String>) -> Self {
63 self.description = Some(description.into());
64 self
65 }
66
67 #[must_use]
69 pub fn with_icon(mut self, icon: Icon) -> Self {
70 self.icons.get_or_insert_with(Vec::new).push(icon);
71 self
72 }
73
74 #[must_use]
76 pub fn with_website_url(mut self, url: impl Into<String>) -> Self {
77 self.website_url = Some(url.into());
78 self
79 }
80}
81
82pub type Implementation = ServerInfo;
88
89#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
91pub struct Icon {
92 pub src: String,
94 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
96 pub mime_type: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub sizes: Option<Vec<String>>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub theme: Option<IconTheme>,
103}
104
105#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
107#[serde(rename_all = "lowercase")]
108pub enum IconTheme {
109 Light,
111 Dark,
113}
114
115impl core::fmt::Display for IconTheme {
116 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117 match self {
118 Self::Light => f.write_str("light"),
119 Self::Dark => f.write_str("dark"),
120 }
121 }
122}
123
124impl Icon {
125 #[must_use]
127 pub fn new(src: impl Into<String>) -> Self {
128 Self {
129 src: src.into(),
130 ..Default::default()
131 }
132 }
133
134 #[must_use]
136 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
137 self.mime_type = Some(mime_type.into());
138 self
139 }
140
141 #[must_use]
143 pub fn with_sizes(mut self, sizes: Vec<impl Into<String>>) -> Self {
144 self.sizes = Some(sizes.into_iter().map(Into::into).collect());
145 self
146 }
147
148 #[must_use]
150 pub fn with_theme(mut self, theme: IconTheme) -> Self {
151 self.theme = Some(theme);
152 self
153 }
154}
155
156#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
160pub struct Tool {
161 pub name: String,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub description: Option<String>,
166 #[serde(rename = "inputSchema")]
168 pub input_schema: ToolInputSchema,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub title: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub icons: Option<Vec<Icon>>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub annotations: Option<ToolAnnotations>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub execution: Option<ToolExecution>,
181 #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
183 pub output_schema: Option<ToolOutputSchema>,
184 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
186 pub meta: Option<HashMap<String, Value>>,
187}
188
189impl Tool {
190 #[must_use]
192 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
193 Self {
194 name: name.into(),
195 description: Some(description.into()),
196 input_schema: ToolInputSchema::default(),
197 ..Default::default()
198 }
199 }
200
201 #[must_use]
203 pub fn with_schema(mut self, schema: ToolInputSchema) -> Self {
204 self.input_schema = schema;
205 self
206 }
207
208 #[must_use]
210 pub fn with_output_schema(mut self, schema: ToolOutputSchema) -> Self {
211 self.output_schema = Some(schema);
212 self
213 }
214
215 #[must_use]
217 pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
218 self.annotations = Some(annotations);
219 self
220 }
221
222 #[must_use]
224 pub fn with_icon(mut self, icon: Icon) -> Self {
225 self.icons.get_or_insert_with(Vec::new).push(icon);
226 self
227 }
228
229 #[must_use]
231 pub fn with_execution(mut self, execution: ToolExecution) -> Self {
232 self.execution = Some(execution);
233 self
234 }
235
236 #[must_use]
238 pub fn read_only(mut self) -> Self {
239 self.annotations = Some(self.annotations.unwrap_or_default().with_read_only(true));
240 self
241 }
242
243 #[must_use]
245 pub fn destructive(mut self) -> Self {
246 self.annotations = Some(self.annotations.unwrap_or_default().with_destructive(true));
247 self
248 }
249}
250
251#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
253pub struct ToolExecution {
254 #[serde(rename = "taskSupport", skip_serializing_if = "Option::is_none")]
256 pub task_support: Option<TaskSupportLevel>,
257}
258
259#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
261#[serde(rename_all = "lowercase")]
262pub enum TaskSupportLevel {
263 Forbidden,
265 Optional,
267 Required,
269}
270
271impl core::fmt::Display for TaskSupportLevel {
272 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
273 match self {
274 Self::Forbidden => f.write_str("forbidden"),
275 Self::Optional => f.write_str("optional"),
276 Self::Required => f.write_str("required"),
277 }
278 }
279}
280
281pub const JSON_SCHEMA_DIALECT_2020_12: &str = "https://json-schema.org/draft/2020-12/schema";
288
289fn default_schema_extras() -> HashMap<String, Value> {
291 let mut m = HashMap::new();
292 m.insert(
293 "$schema".to_string(),
294 Value::String(JSON_SCHEMA_DIALECT_2020_12.to_string()),
295 );
296 m
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
305pub struct ToolInputSchema {
306 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
308 pub schema_type: Option<Value>,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub properties: Option<Value>,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub required: Option<Vec<String>>,
315 #[serde(
317 rename = "additionalProperties",
318 skip_serializing_if = "Option::is_none"
319 )]
320 pub additional_properties: Option<Value>,
321 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
323 pub extra_keywords: HashMap<String, Value>,
324}
325
326impl Default for ToolInputSchema {
327 fn default() -> Self {
328 Self {
329 schema_type: Some(Value::String("object".into())),
330 properties: None,
331 required: None,
332 additional_properties: Some(Value::Bool(false)),
333 extra_keywords: default_schema_extras(),
334 }
335 }
336}
337
338impl ToolInputSchema {
339 #[must_use]
341 pub fn empty() -> Self {
342 Self::default()
343 }
344
345 #[must_use]
350 pub fn from_value(value: Value) -> Self {
351 serde_json::from_value(value).unwrap_or_default()
352 }
353
354 #[must_use]
356 pub fn properties_as_object(&self) -> Option<&serde_json::Map<String, Value>> {
357 self.properties.as_ref().and_then(|v| v.as_object())
358 }
359
360 #[must_use]
362 pub fn with_properties(properties: HashMap<String, Value>) -> Self {
363 let obj: serde_json::Map<String, Value> = properties.into_iter().collect();
364 Self {
365 schema_type: Some(Value::String("object".into())),
366 properties: Some(Value::Object(obj)),
367 required: None,
368 additional_properties: None,
369 extra_keywords: default_schema_extras(),
370 }
371 }
372
373 #[must_use]
375 pub fn with_required_properties(
376 properties: HashMap<String, Value>,
377 required: Vec<String>,
378 ) -> Self {
379 let obj: serde_json::Map<String, Value> = properties.into_iter().collect();
380 Self {
381 schema_type: Some(Value::String("object".into())),
382 properties: Some(Value::Object(obj)),
383 required: Some(required),
384 additional_properties: Some(Value::Bool(false)),
385 extra_keywords: default_schema_extras(),
386 }
387 }
388
389 #[must_use]
391 pub fn add_property(mut self, name: impl Into<String>, property: Value) -> Self {
392 let obj = match self.properties.take() {
393 Some(Value::Object(m)) => m,
394 _ => serde_json::Map::new(),
395 };
396 let mut obj = obj;
397 obj.insert(name.into(), property);
398 self.properties = Some(Value::Object(obj));
399 self
400 }
401
402 #[must_use]
404 pub fn require_property(mut self, name: impl Into<String>) -> Self {
405 let name = name.into();
406 let required = self.required.get_or_insert_with(Vec::new);
407 if !required.contains(&name) {
408 required.push(name);
409 }
410 self
411 }
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
419pub struct ToolOutputSchema {
420 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
422 pub schema_type: Option<Value>,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub properties: Option<Value>,
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub required: Option<Vec<String>>,
429 #[serde(
431 rename = "additionalProperties",
432 skip_serializing_if = "Option::is_none"
433 )]
434 pub additional_properties: Option<Value>,
435 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
437 pub extra_keywords: HashMap<String, Value>,
438}
439
440impl Default for ToolOutputSchema {
441 fn default() -> Self {
442 Self {
443 schema_type: Some(Value::String("object".into())),
444 properties: None,
445 required: None,
446 additional_properties: None,
447 extra_keywords: default_schema_extras(),
448 }
449 }
450}
451
452impl ToolOutputSchema {
453 #[must_use]
455 pub fn empty() -> Self {
456 Self::default()
457 }
458
459 #[must_use]
461 pub fn from_value(value: Value) -> Self {
462 serde_json::from_value(value).unwrap_or_default()
463 }
464
465 #[must_use]
467 pub fn properties_as_object(&self) -> Option<&serde_json::Map<String, Value>> {
468 self.properties.as_ref().and_then(|v| v.as_object())
469 }
470}
471
472#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
474pub struct ToolAnnotations {
475 #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
477 pub read_only_hint: Option<bool>,
478 #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
480 pub destructive_hint: Option<bool>,
481 #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
483 pub idempotent_hint: Option<bool>,
484 #[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
486 pub open_world_hint: Option<bool>,
487 #[serde(skip_serializing_if = "Option::is_none")]
489 pub title: Option<String>,
490}
491
492impl ToolAnnotations {
493 #[must_use]
495 pub fn with_read_only(mut self, value: bool) -> Self {
496 self.read_only_hint = Some(value);
497 self
498 }
499
500 #[must_use]
502 pub fn with_destructive(mut self, value: bool) -> Self {
503 self.destructive_hint = Some(value);
504 self
505 }
506
507 #[must_use]
509 pub fn with_idempotent(mut self, value: bool) -> Self {
510 self.idempotent_hint = Some(value);
511 self
512 }
513
514 #[must_use]
516 pub fn with_open_world(mut self, value: bool) -> Self {
517 self.open_world_hint = Some(value);
518 self
519 }
520}
521
522#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
526pub struct Resource {
527 pub uri: String,
529 pub name: String,
531 #[serde(skip_serializing_if = "Option::is_none")]
533 pub description: Option<String>,
534 #[serde(skip_serializing_if = "Option::is_none")]
536 pub title: Option<String>,
537 #[serde(skip_serializing_if = "Option::is_none")]
539 pub icons: Option<Vec<Icon>>,
540 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
542 pub mime_type: Option<String>,
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub annotations: Option<ResourceAnnotations>,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub size: Option<u64>,
549 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
551 pub meta: Option<HashMap<String, Value>>,
552}
553
554impl Resource {
555 #[must_use]
557 pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
558 Self {
559 uri: uri.into(),
560 name: name.into(),
561 ..Default::default()
562 }
563 }
564
565 #[must_use]
567 pub fn with_description(mut self, description: impl Into<String>) -> Self {
568 self.description = Some(description.into());
569 self
570 }
571
572 #[must_use]
574 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
575 self.mime_type = Some(mime_type.into());
576 self
577 }
578
579 #[must_use]
581 pub fn with_size(mut self, size: u64) -> Self {
582 self.size = Some(size);
583 self
584 }
585
586 #[must_use]
588 pub fn with_icon(mut self, icon: Icon) -> Self {
589 self.icons.get_or_insert_with(Vec::new).push(icon);
590 self
591 }
592}
593
594#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
598pub struct ResourceAnnotations {
599 #[serde(skip_serializing_if = "Option::is_none")]
601 pub audience: Option<Vec<crate::Role>>,
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub priority: Option<f64>,
605 #[serde(rename = "lastModified", skip_serializing_if = "Option::is_none")]
607 pub last_modified: Option<String>,
608}
609
610#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
614pub struct ResourceTemplate {
615 #[serde(rename = "uriTemplate")]
617 pub uri_template: String,
618 pub name: String,
620 #[serde(skip_serializing_if = "Option::is_none")]
622 pub description: Option<String>,
623 #[serde(skip_serializing_if = "Option::is_none")]
625 pub title: Option<String>,
626 #[serde(skip_serializing_if = "Option::is_none")]
628 pub icons: Option<Vec<Icon>>,
629 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
631 pub mime_type: Option<String>,
632 #[serde(skip_serializing_if = "Option::is_none")]
634 pub annotations: Option<ResourceAnnotations>,
635 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
637 pub meta: Option<HashMap<String, Value>>,
638}
639
640impl ResourceTemplate {
641 #[must_use]
646 pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
647 Self {
648 uri_template: uri_template.into(),
649 name: name.into(),
650 ..Default::default()
651 }
652 }
653
654 pub fn try_new(
661 uri_template: impl Into<String>,
662 name: impl Into<String>,
663 ) -> Result<Self, &'static str> {
664 let uri_template = uri_template.into();
665 validate_uri_template(&uri_template)?;
666 Ok(Self {
667 uri_template,
668 name: name.into(),
669 ..Default::default()
670 })
671 }
672
673 #[must_use]
675 pub fn with_description(mut self, description: impl Into<String>) -> Self {
676 self.description = Some(description.into());
677 self
678 }
679
680 #[must_use]
682 pub fn with_icon(mut self, icon: Icon) -> Self {
683 self.icons.get_or_insert_with(Vec::new).push(icon);
684 self
685 }
686}
687
688pub fn validate_uri_template(s: &str) -> Result<(), &'static str> {
693 let mut depth = 0i32;
694 let mut current_expr_start: Option<usize> = None;
695 let bytes = s.as_bytes();
696 for (i, ch) in s.char_indices() {
697 match ch {
698 '{' => {
699 depth += 1;
700 if depth > 1 {
701 return Err("URI template: nested '{' not allowed in RFC 6570");
702 }
703 current_expr_start = Some(i + 1);
704 }
705 '}' => {
706 depth -= 1;
707 if depth < 0 {
708 return Err("URI template: unbalanced '}' (no matching '{')");
709 }
710 if let Some(start) = current_expr_start {
711 let body = &bytes[start..i];
712 if body.is_empty() {
713 return Err("URI template: empty expression `{}`");
714 }
715 let body_start =
717 if matches!(body[0], b'+' | b'#' | b'.' | b'/' | b';' | b'?' | b'&') {
718 1
719 } else {
720 0
721 };
722 let var_bytes = &body[body_start..];
723 if var_bytes.is_empty() {
724 return Err("URI template: operator without variable name");
725 }
726 let first = var_bytes[0];
727 if !(first.is_ascii_alphabetic() || first == b'_') {
728 return Err(
729 "URI template: variable name must start with a letter or underscore",
730 );
731 }
732 for &b in var_bytes {
733 if !(b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b',') {
734 return Err(
735 "URI template: invalid character in variable name (allowed: ALPHA / DIGIT / '_' / '.' / ',')",
736 );
737 }
738 }
739 }
740 current_expr_start = None;
741 }
742 _ => {}
743 }
744 }
745 if depth != 0 {
746 return Err("URI template: unbalanced '{' (missing closing '}')");
747 }
748 Ok(())
749}
750
751#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
755pub struct Prompt {
756 pub name: String,
758 #[serde(skip_serializing_if = "Option::is_none")]
760 pub description: Option<String>,
761 #[serde(skip_serializing_if = "Option::is_none")]
763 pub title: Option<String>,
764 #[serde(skip_serializing_if = "Option::is_none")]
766 pub icons: Option<Vec<Icon>>,
767 #[serde(skip_serializing_if = "Option::is_none")]
769 pub arguments: Option<Vec<PromptArgument>>,
770 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
772 pub meta: Option<HashMap<String, Value>>,
773}
774
775impl Prompt {
776 #[must_use]
778 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
779 Self {
780 name: name.into(),
781 description: Some(description.into()),
782 ..Default::default()
783 }
784 }
785
786 #[must_use]
788 pub fn with_argument(mut self, arg: PromptArgument) -> Self {
789 self.arguments.get_or_insert_with(Vec::new).push(arg);
790 self
791 }
792
793 #[must_use]
795 pub fn with_required_arg(
796 self,
797 name: impl Into<String>,
798 description: impl Into<String>,
799 ) -> Self {
800 self.with_argument(PromptArgument::required(name, description))
801 }
802
803 #[must_use]
805 pub fn with_optional_arg(
806 self,
807 name: impl Into<String>,
808 description: impl Into<String>,
809 ) -> Self {
810 self.with_argument(PromptArgument::optional(name, description))
811 }
812
813 #[must_use]
815 pub fn with_icon(mut self, icon: Icon) -> Self {
816 self.icons.get_or_insert_with(Vec::new).push(icon);
817 self
818 }
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
825pub struct PromptArgument {
826 pub name: String,
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub title: Option<String>,
831 #[serde(skip_serializing_if = "Option::is_none")]
833 pub description: Option<String>,
834 #[serde(skip_serializing_if = "Option::is_none")]
836 pub required: Option<bool>,
837}
838
839impl PromptArgument {
840 #[must_use]
842 pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
843 Self {
844 name: name.into(),
845 title: None,
846 description: Some(description.into()),
847 required: Some(true),
848 }
849 }
850
851 #[must_use]
853 pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
854 Self {
855 name: name.into(),
856 title: None,
857 description: Some(description.into()),
858 required: Some(false),
859 }
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use super::*;
866
867 #[test]
868 fn test_server_info() {
869 let info = ServerInfo::new("my-server", "1.0.0")
870 .with_title("My Server")
871 .with_description("A test server")
872 .with_icon(Icon::new("https://example.com/icon.png"));
873
874 assert_eq!(info.name, "my-server");
875 assert_eq!(info.version, "1.0.0");
876 assert_eq!(info.title, Some("My Server".into()));
877 assert_eq!(info.icons.as_ref().unwrap().len(), 1);
878 assert_eq!(
879 info.icons.as_ref().unwrap()[0].src,
880 "https://example.com/icon.png"
881 );
882 }
883
884 #[test]
885 fn test_tool_builder() {
886 let tool = Tool::new("add", "Add two numbers").with_annotations(
888 ToolAnnotations::default()
889 .with_read_only(true)
890 .with_idempotent(true),
891 );
892
893 assert_eq!(tool.name, "add");
894 assert!(tool.annotations.as_ref().unwrap().read_only_hint.unwrap());
895 assert!(tool.annotations.as_ref().unwrap().idempotent_hint.unwrap());
896 }
897
898 #[test]
899 fn test_tool_read_only() {
900 let tool = Tool::new("query", "Query data").read_only();
901 assert!(tool.annotations.as_ref().unwrap().read_only_hint.unwrap());
902 }
903
904 #[test]
905 fn test_tool_destructive() {
906 let tool = Tool::new("delete", "Delete data").destructive();
907 assert!(tool.annotations.as_ref().unwrap().destructive_hint.unwrap());
908 }
909
910 #[test]
911 fn test_resource_builder() {
912 let resource = Resource::new("file:///test.txt", "test")
913 .with_description("A test file")
914 .with_mime_type("text/plain");
915
916 assert_eq!(resource.uri, "file:///test.txt");
917 assert_eq!(resource.mime_type, Some("text/plain".into()));
918 }
919
920 #[test]
921 fn test_prompt_builder() {
922 let prompt = Prompt::new("greeting", "A greeting prompt")
923 .with_required_arg("name", "Name to greet")
924 .with_optional_arg("style", "Greeting style");
925
926 assert_eq!(prompt.name, "greeting");
927 assert_eq!(prompt.arguments.as_ref().unwrap().len(), 2);
928 assert!(prompt.arguments.as_ref().unwrap()[0].required.unwrap());
929 assert!(!prompt.arguments.as_ref().unwrap()[1].required.unwrap());
930 }
931
932 #[test]
933 fn test_tool_serde() {
934 let tool = Tool::new("test", "Test tool");
935 let json = serde_json::to_string(&tool).unwrap();
936 assert!(json.contains("\"name\":\"test\""));
937 assert!(json.contains("\"inputSchema\""));
938 }
939}