Skip to main content

mcp/
wire.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! MCP wire types — the Model Context Protocol message surface, shared by the
3//! client and the served-MCP side.
4//!
5//! Method/notification names are constants (typos become compile errors).
6//! Result/param structs use `camelCase` to match the spec. `content[]` and
7//! resource `contents[]` are kept as `Vec<Value>` with text-extraction helpers
8//! rather than a brittle tagged enum, so an unknown content type from a newer
9//! server is preserved, not a parse error (forward-compat).
10//!
11//! The protocol version + era model lives in [`crate::version`]; it is re-exported
12//! here so `mcp::wire::{PROTOCOL_VERSION, negotiate_version, …}` resolves.
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17pub use crate::version::*;
18
19/// Method + notification names. Constants, so a typo is a compile error rather
20/// than a `-32601` at runtime.
21pub mod method {
22    pub const INITIALIZE: &str = "initialize";
23    pub const INITIALIZED: &str = "notifications/initialized";
24    pub const PING: &str = "ping";
25    pub const TOOLS_LIST: &str = "tools/list";
26    pub const TOOLS_CALL: &str = "tools/call";
27    pub const RESOURCES_LIST: &str = "resources/list";
28    pub const RESOURCES_READ: &str = "resources/read";
29    pub const RESOURCES_SUBSCRIBE: &str = "resources/subscribe";
30    pub const RESOURCES_UNSUBSCRIBE: &str = "resources/unsubscribe";
31    pub const RESOURCES_TEMPLATES_LIST: &str = "resources/templates/list";
32    pub const PROMPTS_LIST: &str = "prompts/list";
33    pub const PROMPTS_GET: &str = "prompts/get";
34    pub const COMPLETION_COMPLETE: &str = "completion/complete";
35    pub const LOGGING_SET_LEVEL: &str = "logging/setLevel";
36
37    // Tasks extension (io.modelcontextprotocol/tasks): async long-running requests.
38    pub const TASKS_GET: &str = "tasks/get";
39    pub const TASKS_UPDATE: &str = "tasks/update";
40    pub const TASKS_CANCEL: &str = "tasks/cancel";
41    pub const NOTIFY_TASKS: &str = "notifications/tasks";
42
43    // Modern (2026-07-28+, stateless) methods.
44    /// Query a server's supported versions + capabilities + identity in one call
45    /// (the stateless replacement for the `initialize` capability exchange).
46    pub const SERVER_DISCOVER: &str = "server/discover";
47    /// Open the long-lived notification stream (its SSE response carries the
48    /// change notifications the client opted in to — the stateless replacement
49    /// for the removed GET SSE stream).
50    pub const SUBSCRIPTIONS_LISTEN: &str = "subscriptions/listen";
51
52    // Notifications (no id, no response).
53    pub const NOTIFY_RESOURCES_UPDATED: &str = "notifications/resources/updated";
54    pub const NOTIFY_RESOURCES_LIST_CHANGED: &str = "notifications/resources/list_changed";
55    pub const NOTIFY_TOOLS_LIST_CHANGED: &str = "notifications/tools/list_changed";
56    pub const NOTIFY_SUBSCRIPTIONS_ACK: &str = "notifications/subscriptions/acknowledged";
57    pub const NOTIFY_CANCELLED: &str = "notifications/cancelled";
58    pub const NOTIFY_PROGRESS: &str = "notifications/progress";
59    pub const NOTIFY_MESSAGE: &str = "notifications/message";
60}
61
62// ---- lifecycle ----
63
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65pub struct Implementation {
66    pub name: String,
67    pub version: String,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub title: Option<String>,
70}
71
72/// Capabilities a client declares. Only declare what the client can actually
73/// service: a server is entitled to call anything advertised here, and a
74/// declared-but-unanswerable capability strands it waiting on a reply.
75#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct ClientCapabilities {
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub experimental: Option<Value>,
79    /// `{}` when this client can deliver a server's `elicitation/create` to a
80    /// human. Omitted otherwise — a server must not ask what we cannot answer.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub elicitation: Option<Value>,
83    /// `{"listChanged": bool}` when this client answers `roots/list`.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub roots: Option<Value>,
86}
87
88#[derive(Debug, Clone, Default, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct InitializeParams {
91    pub protocol_version: String,
92    pub capabilities: ClientCapabilities,
93    pub client_info: Implementation,
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct InitializeResult {
99    pub protocol_version: String,
100    #[serde(default)]
101    pub capabilities: ServerCapabilities,
102    #[serde(default)]
103    pub server_info: Option<Implementation>,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub instructions: Option<String>,
106}
107
108/// Result of `server/discover` (modern era): the server's supported protocol
109/// versions, capabilities, and identity in a single call — the stateless
110/// replacement for the legacy `initialize` capability exchange. `resultType` and
111/// the caching fields (`ttlMs`/`cacheScope`) are carried for forward-compat.
112#[derive(Debug, Clone, Default, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct DiscoverResult {
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub result_type: Option<String>,
117    #[serde(default)]
118    pub supported_versions: Vec<String>,
119    #[serde(default)]
120    pub capabilities: ServerCapabilities,
121    #[serde(default)]
122    pub server_info: Option<Implementation>,
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub instructions: Option<String>,
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub ttl_ms: Option<u64>,
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub cache_scope: Option<String>,
129}
130
131/// What a server says it can do. Every call is gated on these, and the gate is
132/// fail-closed — an absent capability is a refusal, not a maybe: no `tools/call`
133/// unless `tools` is present; no `resources/subscribe` unless
134/// `resources.subscribe == Some(true)`.
135#[derive(Debug, Clone, Default, Serialize, Deserialize)]
136pub struct ServerCapabilities {
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub tools: Option<ToolsCapability>,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub resources: Option<ResourcesCapability>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub prompts: Option<Value>,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub logging: Option<Value>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub completions: Option<Value>,
147}
148
149#[derive(Debug, Clone, Default, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct ToolsCapability {
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub list_changed: Option<bool>,
154}
155
156#[derive(Debug, Clone, Default, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct ResourcesCapability {
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub subscribe: Option<bool>,
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub list_changed: Option<bool>,
163}
164
165impl ServerCapabilities {
166    pub fn supports_tools(&self) -> bool {
167        self.tools.is_some()
168    }
169    pub fn supports_resources(&self) -> bool {
170        self.resources.is_some()
171    }
172    pub fn supports_subscribe(&self) -> bool {
173        self.resources
174            .as_ref()
175            .and_then(|r| r.subscribe)
176            .unwrap_or(false)
177    }
178    pub fn supports_prompts(&self) -> bool {
179        self.prompts.is_some()
180    }
181    pub fn supports_completions(&self) -> bool {
182        self.completions.is_some()
183    }
184}
185
186// ---- tools ----
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct Tool {
191    pub name: String,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub title: Option<String>,
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub description: Option<String>,
196    /// JSON Schema for the tool's arguments.
197    pub input_schema: Value,
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub output_schema: Option<Value>,
200}
201
202#[derive(Debug, Clone, Default, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct ListToolsResult {
205    pub tools: Vec<Tool>,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub next_cursor: Option<String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct CallToolParams {
212    pub name: String,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub arguments: Option<Value>,
215}
216
217/// Result of `tools/call`. `is_error: true` is a **tool-domain** failure (fed
218/// to the model as an observation so it can adapt), distinct from a JSON-RPC
219/// transport error, which fails the call outright.
220#[derive(Debug, Clone, Default, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct CallToolResult {
223    #[serde(default)]
224    pub content: Vec<Value>,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub is_error: Option<bool>,
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub structured_content: Option<Value>,
229}
230
231impl CallToolResult {
232    pub fn is_error(&self) -> bool {
233        self.is_error.unwrap_or(false)
234    }
235    /// Concatenate the `text` parts of `content[]` — what the loop feeds back
236    /// to the model. Non-text parts (image/audio/resource) are summarized by
237    /// type so the model knows they were returned.
238    pub fn text(&self) -> String {
239        content_text(&self.content)
240    }
241}
242
243// ---- resources ----
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct Resource {
248    pub uri: String,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub name: Option<String>,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub title: Option<String>,
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub description: Option<String>,
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub mime_type: Option<String>,
257}
258
259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct ListResourcesResult {
262    pub resources: Vec<Resource>,
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub next_cursor: Option<String>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct ReadResourceParams {
269    pub uri: String,
270}
271
272#[derive(Debug, Clone, Default, Serialize, Deserialize)]
273pub struct ReadResourceResult {
274    /// Each entry is a `{uri, mimeType, text}` or `{uri, mimeType, blob}`
275    /// object; kept as `Value` for forward-compat. Use [`Self::text`].
276    #[serde(default)]
277    pub contents: Vec<Value>,
278}
279
280impl ReadResourceResult {
281    pub fn text(&self) -> String {
282        content_text(&self.contents)
283    }
284}
285
286/// A resource **template** (a parameterized `uriTemplate`, RFC 6570) a server
287/// offers via `resources/templates/list` — distinct from a concrete [`Resource`].
288#[derive(Debug, Clone, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct ResourceTemplate {
291    pub uri_template: String,
292    #[serde(default, skip_serializing_if = "Option::is_none")]
293    pub name: Option<String>,
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub title: Option<String>,
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub description: Option<String>,
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub mime_type: Option<String>,
300}
301
302#[derive(Debug, Clone, Default, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct ListResourceTemplatesResult {
305    pub resource_templates: Vec<ResourceTemplate>,
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub next_cursor: Option<String>,
308}
309
310/// `resources/subscribe` / `resources/unsubscribe` params. Per-URI only: a
311/// subscription names one concrete resource, never a [`ResourceTemplate`], since
312/// a template matches an open-ended set with no item to watch.
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct SubscribeParams {
315    pub uri: String,
316}
317
318/// Payload of `notifications/resources/updated` — **URI only**, never a diff.
319/// The notification is a wake-up, not the data: the reader re-reads the URI on
320/// wake, so a burst of updates collapses into one read of the current state and
321/// a missed notification costs freshness, not correctness.
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct ResourceUpdatedParams {
324    pub uri: String,
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub title: Option<String>,
327}
328
329// ---- tasks extension (io.modelcontextprotocol/tasks) ----
330
331/// The tasks extension identifier — advertised in `capabilities.extensions` to
332/// opt into task-augmented (async long-running) requests.
333pub const TASKS_EXTENSION: &str = "io.modelcontextprotocol/tasks";
334
335/// A durable async-task handle (the tasks extension). A supported request (e.g.
336/// `tools/call`) may return one (`resultType: "task"`) instead of blocking; the
337/// client polls [`method::TASKS_GET`] until a terminal `status`.
338#[derive(Debug, Clone, Default, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct Task {
341    pub task_id: String,
342    /// `working` | `input_required` | `completed` | `failed` | `cancelled`.
343    #[serde(default)]
344    pub status: String,
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub status_message: Option<String>,
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub ttl_ms: Option<u64>,
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub poll_interval_ms: Option<u64>,
351    /// On `completed`: what the original request would have returned.
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub result: Option<Value>,
354    /// On `failed`: the JSON-RPC error.
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub error: Option<Value>,
357    /// On `input_required`: the server's outstanding input requests (MRTR).
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub input_requests: Option<Value>,
360}
361
362impl Task {
363    /// A terminal status (`completed`/`failed`/`cancelled`) — polling stops.
364    pub fn is_terminal(&self) -> bool {
365        matches!(self.status.as_str(), "completed" | "failed" | "cancelled")
366    }
367    pub fn needs_input(&self) -> bool {
368        self.status == "input_required"
369    }
370}
371
372/// If a result value is a task handle (`resultType: "task"`), parse it — the
373/// polymorphic shape a task-augmented request returns instead of its normal result.
374pub fn as_task_result(result: &Value) -> Option<Task> {
375    if result.get("resultType").and_then(Value::as_str) == Some("task") {
376        serde_json::from_value(result.clone()).ok()
377    } else {
378        None
379    }
380}
381
382// ---- prompts ----
383
384/// A prompt template a server offers via `prompts/list`. `arguments` describe
385/// the template's fill-ins.
386#[derive(Debug, Clone, Serialize, Deserialize)]
387#[serde(rename_all = "camelCase")]
388pub struct Prompt {
389    pub name: String,
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    pub title: Option<String>,
392    #[serde(default, skip_serializing_if = "Option::is_none")]
393    pub description: Option<String>,
394    #[serde(default, skip_serializing_if = "Vec::is_empty")]
395    pub arguments: Vec<PromptArgument>,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct PromptArgument {
401    pub name: String,
402    #[serde(default, skip_serializing_if = "Option::is_none")]
403    pub title: Option<String>,
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub description: Option<String>,
406    #[serde(default, skip_serializing_if = "Option::is_none")]
407    pub required: Option<bool>,
408}
409
410#[derive(Debug, Clone, Default, Serialize, Deserialize)]
411#[serde(rename_all = "camelCase")]
412pub struct ListPromptsResult {
413    pub prompts: Vec<Prompt>,
414    #[serde(default, skip_serializing_if = "Option::is_none")]
415    pub next_cursor: Option<String>,
416}
417
418/// `prompts/get` params — the template name + its argument fills (all strings).
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct GetPromptParams {
421    pub name: String,
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    pub arguments: Option<Value>,
424}
425
426/// `prompts/get` result — the rendered messages. `messages[]` is kept as
427/// `Vec<Value>` (each `{role, content}`) for forward-compat with content types.
428#[derive(Debug, Clone, Default, Serialize, Deserialize)]
429pub struct GetPromptResult {
430    #[serde(default, skip_serializing_if = "Option::is_none")]
431    pub description: Option<String>,
432    #[serde(default)]
433    pub messages: Vec<Value>,
434}
435
436// ---- completion ----
437
438/// `completion/complete` params: what to complete (a `ref` to a prompt or
439/// resource template) and the argument being typed. Kept as `Value` — the `ref`
440/// shape varies by target and revision (forward-compat).
441#[derive(Debug, Clone, Serialize, Deserialize)]
442#[serde(rename_all = "camelCase")]
443pub struct CompleteParams {
444    #[serde(rename = "ref")]
445    pub reference: Value,
446    pub argument: Value,
447    #[serde(default, skip_serializing_if = "Option::is_none")]
448    pub context: Option<Value>,
449}
450
451#[derive(Debug, Clone, Default, Serialize, Deserialize)]
452pub struct CompleteResult {
453    #[serde(default)]
454    pub completion: Completion,
455}
456
457#[derive(Debug, Clone, Default, Serialize, Deserialize)]
458#[serde(rename_all = "camelCase")]
459pub struct Completion {
460    #[serde(default)]
461    pub values: Vec<String>,
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub total: Option<u64>,
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub has_more: Option<bool>,
466}
467
468/// Extract human-readable text from an MCP `content[]` / `contents[]` array.
469/// Text parts are concatenated; other known parts are noted by type.
470fn content_text(items: &[Value]) -> String {
471    let mut parts: Vec<String> = Vec::new();
472    for item in items {
473        match item.get("type").and_then(Value::as_str) {
474            // Tool text parts and resource `contents[]` (which omit `type` but
475            // carry `text`) both land here.
476            Some("text") | None => {
477                if let Some(t) = item.get("text").and_then(Value::as_str) {
478                    parts.push(t.to_string());
479                }
480            }
481            Some(other) => parts.push(format!("[{other} content]")),
482        }
483    }
484    parts.join("\n")
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use serde_json::json;
491
492    #[test]
493    fn initialize_result_parses_capabilities() {
494        let json = r#"{
495            "protocolVersion": "2025-11-25",
496            "capabilities": {"tools": {"listChanged": true}, "resources": {"subscribe": true}},
497            "serverInfo": {"name": "fs", "version": "1.0"}
498        }"#;
499        let r: InitializeResult = serde_json::from_str(json).unwrap();
500        assert_eq!(r.protocol_version, "2025-11-25");
501        assert!(r.capabilities.supports_tools());
502        assert!(r.capabilities.supports_subscribe());
503    }
504
505    #[test]
506    fn capability_gating_defaults_closed() {
507        let caps = ServerCapabilities::default();
508        assert!(!caps.supports_tools());
509        assert!(!caps.supports_subscribe());
510        // tools present but subscribe absent -> subscribe denied
511        let json = r#"{"tools": {}, "resources": {"listChanged": true}}"#;
512        let caps: ServerCapabilities = serde_json::from_str(json).unwrap();
513        assert!(caps.supports_tools());
514        assert!(!caps.supports_subscribe());
515    }
516
517    #[test]
518    fn call_tool_result_text_and_error() {
519        let json = r#"{"content": [{"type": "text", "text": "hello"}, {"type": "image", "data": "..."}], "isError": false}"#;
520        let r: CallToolResult = serde_json::from_str(json).unwrap();
521        assert!(!r.is_error());
522        assert!(r.text().contains("hello"));
523        assert!(r.text().contains("[image content]"));
524    }
525
526    #[test]
527    fn updated_notification_is_uri_only() {
528        let json = r#"{"uri": "file:///data/in.json"}"#;
529        let p: ResourceUpdatedParams = serde_json::from_str(json).unwrap();
530        assert_eq!(p.uri, "file:///data/in.json");
531        assert!(p.title.is_none());
532    }
533
534    #[test]
535    fn tool_list_pagination_cursor() {
536        let json = r#"{"tools": [{"name": "read_file", "inputSchema": {"type": "object"}}], "nextCursor": "abc"}"#;
537        let r: ListToolsResult = serde_json::from_str(json).unwrap();
538        assert_eq!(r.tools.len(), 1);
539        assert_eq!(r.next_cursor.as_deref(), Some("abc"));
540    }
541
542    #[test]
543    fn discover_result_parses() {
544        let json = r#"{
545            "resultType": "complete",
546            "supportedVersions": ["2026-07-28", "2025-11-25"],
547            "capabilities": {"tools": {}, "resources": {"subscribe": true}, "prompts": {}},
548            "serverInfo": {"name": "s", "version": "1"},
549            "ttlMs": 3600000, "cacheScope": "public"
550        }"#;
551        let d: DiscoverResult = serde_json::from_str(json).unwrap();
552        assert_eq!(d.supported_versions, ["2026-07-28", "2025-11-25"]);
553        assert!(d.capabilities.supports_tools());
554        assert!(d.capabilities.supports_subscribe());
555        assert!(d.capabilities.supports_prompts());
556        assert_eq!(d.ttl_ms, Some(3_600_000));
557    }
558
559    #[test]
560    fn task_result_detected_and_lifecycle() {
561        // A tools/call result that is actually a task handle.
562        let create = json!({"resultType": "task", "taskId": "t-1", "status": "working",
563            "pollIntervalMs": 250, "ttlMs": 60000});
564        let t = as_task_result(&create).expect("is a task result");
565        assert_eq!(t.task_id, "t-1");
566        assert_eq!(t.poll_interval_ms, Some(250));
567        assert!(!t.is_terminal());
568        // A normal (non-task) result is not a task.
569        assert!(as_task_result(&json!({"content": []})).is_none());
570        // Terminal / input states.
571        let done: Task = serde_json::from_value(
572            json!({"taskId": "t-1", "status": "completed", "result": {"content": []}}),
573        )
574        .unwrap();
575        assert!(done.is_terminal() && !done.needs_input());
576        let ask: Task = serde_json::from_value(
577            json!({"taskId": "t-1", "status": "input_required", "inputRequests": {}}),
578        )
579        .unwrap();
580        assert!(ask.needs_input() && !ask.is_terminal());
581    }
582
583    #[test]
584    fn prompts_and_completion_parse() {
585        let list: ListPromptsResult = serde_json::from_str(
586            r#"{"prompts": [{"name": "greet", "arguments": [{"name": "who", "required": true}]}]}"#,
587        )
588        .unwrap();
589        assert_eq!(list.prompts[0].name, "greet");
590        assert_eq!(list.prompts[0].arguments[0].name, "who");
591        assert_eq!(list.prompts[0].arguments[0].required, Some(true));
592
593        let got: GetPromptResult = serde_json::from_str(
594            r#"{"description": "d", "messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}]}"#,
595        )
596        .unwrap();
597        assert_eq!(got.messages.len(), 1);
598
599        let comp: CompleteResult = serde_json::from_str(
600            r#"{"completion": {"values": ["alice", "bob"], "hasMore": false}}"#,
601        )
602        .unwrap();
603        assert_eq!(comp.completion.values, ["alice", "bob"]);
604        assert_eq!(comp.completion.has_more, Some(false));
605    }
606}