1pub mod grammar;
27
28use std::{
29 io::{self, BufRead, Write},
30 mem, process, str,
31 sync::mpsc::Sender,
32 thread::{self, JoinHandle},
33};
34
35use anyhow::{Context, Error, Result};
36use serde::Serialize;
37use serde_json::{Map, Value, json};
38
39use crate::{
40 error::{
41 ErrorCode, ErrorReport, NoErrorCode, failure_status, partial_outcome_of, usage_invalid,
42 },
43 printer::CapturePrinter,
44 session::claim_stdin,
45};
46
47pub const PROTOCOL_VERSIONS: &[&str] = &["2025-06-18"];
52
53pub const PARSE_ERROR: i64 = -32700;
55
56pub const INVALID_REQUEST: i64 = -32600;
58
59pub const METHOD_NOT_FOUND: i64 = -32601;
61
62pub const INVALID_PARAMS: i64 = -32602;
64
65#[derive(Clone, Copy, Debug)]
67pub struct ServerInfo<'a> {
68 pub name: &'a str,
70 pub version: &'a str,
72 pub instructions: Option<&'a str>,
74}
75
76#[derive(Debug)]
78pub enum Incoming {
79 Reply(Value),
81 Call(ToolCall),
83 Ignore,
85}
86
87#[derive(Clone, Debug)]
90pub struct ToolCall {
91 pub id: Value,
93 pub name: String,
95 pub arguments: Map<String, Value>,
97}
98
99impl ToolCall {
100 pub fn respond(&self, result: ToolResult) -> Value {
102 json!({ "jsonrpc": "2.0", "id": self.id, "result": result })
103 }
104}
105
106#[derive(Clone, Debug, Default, Serialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ToolResult {
113 pub content: Vec<Value>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub structured_content: Option<Value>,
118 #[serde(skip_serializing_if = "is_false")]
120 pub is_error: bool,
121}
122
123impl ToolResult {
124 pub fn success(value: Value) -> Self {
127 Self {
128 content: vec![text(value.to_string())],
129 structured_content: value.is_object().then_some(value),
130 is_error: false,
131 }
132 }
133
134 pub fn failure(report: Value) -> Self {
136 Self {
137 content: vec![text(report.to_string())],
138 structured_content: None,
139 is_error: true,
140 }
141 }
142
143 pub fn error<C: ErrorCode>(err: Error) -> Self {
146 Self::failure(error_object::<C>(err))
147 }
148
149 pub fn printed<C: ErrorCode>(mut printer: CapturePrinter, result: Result<()>) -> Self {
156 let partial = match result {
157 Ok(()) => false,
158 Err(err) if partial_outcome_of(&err).is_some() => true,
159 Err(err) => return Self::error::<C>(err),
160 };
161
162 let events = mem::take(&mut printer.events);
163
164 let value = match printer.into_single() {
165 Ok(value) => value,
166 Err(err) => return Self::error::<C>(err),
167 };
168
169 let mut result = Self::success(value);
170 let events = events.into_iter().map(|event| text(event.to_string()));
171 result.content.splice(0..0, events);
172 result.is_error = partial;
173 result
174 }
175}
176
177#[derive(Debug)]
179pub enum Input {
180 Line(Vec<u8>),
182 Eof,
184}
185
186pub fn read(line: &[u8], info: &ServerInfo, tools: &[Value]) -> Incoming {
199 let Ok(line) = str::from_utf8(line) else {
200 let message = "Parse error: the message is not UTF-8";
201 return Incoming::Reply(error(Value::Null, PARSE_ERROR, message));
202 };
203
204 if line.trim().is_empty() {
205 return Incoming::Ignore;
206 }
207
208 let message = match serde_json::from_str(line) {
209 Ok(Value::Object(message)) => message,
210 Ok(_) => {
211 let message =
212 "Invalid request: a message is one JSON object, batches are not supported";
213 return Incoming::Reply(error(Value::Null, INVALID_REQUEST, message));
214 }
215 Err(err) => {
216 let message = format!("Parse error: {err}");
217 return Incoming::Reply(error(Value::Null, PARSE_ERROR, message));
218 }
219 };
220
221 let id = match message.get("id") {
222 None => None,
223 Some(id @ (Value::String(_) | Value::Number(_))) => Some(id.clone()),
224 Some(_) => {
225 let message = "Invalid request: the id is not a string or a number";
226 return Incoming::Reply(error(Value::Null, INVALID_REQUEST, message));
227 }
228 };
229
230 let invalid = |message: &str| {
231 let id = id.clone().unwrap_or(Value::Null);
232 Incoming::Reply(error(id, INVALID_REQUEST, message))
233 };
234
235 if message.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
236 return invalid("Invalid request: jsonrpc is not \"2.0\"");
237 }
238
239 let Some(method) = message.get("method").and_then(Value::as_str) else {
241 return invalid("Invalid request: the method is missing or not a string");
242 };
243
244 let Some(id) = id else {
245 return Incoming::Ignore;
246 };
247
248 let params = message.get("params");
249
250 let result = match method {
251 "initialize" => initialize(info, params),
252 "ping" => json!({}),
253 "tools/list" => json!({ "tools": tools }),
254 "tools/call" => return call(id, params, tools),
255 method => {
256 let message = format!("Method not found: {method}");
257 return Incoming::Reply(error(id, METHOD_NOT_FOUND, message));
258 }
259 };
260
261 Incoming::Reply(json!({ "jsonrpc": "2.0", "id": id, "result": result }))
262}
263
264pub fn answer(
267 line: &[u8],
268 info: &ServerInfo,
269 tools: &[Value],
270 run: impl FnOnce(&ToolCall) -> ToolResult,
271) -> Option<Value> {
272 match read(line, info, tools) {
273 Incoming::Reply(reply) => Some(reply),
274 Incoming::Call(call) => Some(call.respond(run(&call))),
275 Incoming::Ignore => None,
276 }
277}
278
279pub fn send(output: &mut impl Write, message: &Value) -> Result<()> {
281 serde_json::to_writer(&mut *output, message).context("Cannot write a message to stdout")?;
282 writeln!(output)?;
283 output.flush()?;
284 Ok(())
285}
286
287pub fn serve(
292 info: &ServerInfo,
293 tools: &[Value],
294 run: impl FnMut(&ToolCall) -> ToolResult,
295) -> Result<()> {
296 claim_stdin();
297 serve_on(&mut io::stdin().lock(), &mut io::stdout(), info, tools, run)
298}
299
300pub fn spawn_stdin_reader<T: Send + 'static>(
306 events: Sender<T>,
307 wrap: impl Fn(Input) -> T + Send + 'static,
308) -> JoinHandle<()> {
309 claim_stdin();
310 thread::spawn(move || read_lines(&mut io::stdin().lock(), &events, wrap))
311}
312
313pub fn exit_on_failure<C: ErrorCode, T>(result: Result<T>) -> T {
321 match result {
322 Ok(value) => value,
323 Err(err) => {
324 if let Some(outcome) = partial_outcome_of(&err) {
325 process::exit(outcome.status());
326 }
327
328 let status = failure_status(&err);
329 eprint!("{}", ErrorReport::<C>::from(err));
330 process::exit(status);
331 }
332 }
333}
334
335pub fn tool_output_schema(schema: &Value) -> Option<&Value> {
341 (schema["type"] == "object").then_some(schema)
342}
343
344pub fn error_object<C: ErrorCode>(err: Error) -> Value {
347 serde_json::to_value(ErrorReport::<C>::from(err))
348 .unwrap_or_else(|err| Value::from(err.to_string()))
349}
350
351pub fn text(text: impl Into<String>) -> Value {
354 json!({ "type": "text", "text": text.into() })
355}
356
357pub(crate) fn unknown_argument<'a>(
360 key: &str,
361 owner: &str,
362 known: impl IntoIterator<Item = &'a str>,
363) -> Error {
364 let known: Vec<String> = known.into_iter().map(|name| format!("`{name}`")).collect();
365
366 if known.is_empty() {
367 usage_invalid(format!("Unknown argument `{key}`, {owner} takes none"))
368 } else {
369 usage_invalid(format!(
370 "Unknown argument `{key}`, {owner} takes {}",
371 known.join(", ")
372 ))
373 }
374}
375
376fn serve_on(
378 input: &mut impl BufRead,
379 output: &mut impl Write,
380 info: &ServerInfo,
381 tools: &[Value],
382 mut run: impl FnMut(&ToolCall) -> ToolResult,
383) -> Result<()> {
384 let mut line = Vec::new();
385
386 loop {
387 line.clear();
388
389 let read = input.read_until(b'\n', &mut line);
390 if read.context("Cannot read a message from stdin")? == 0 {
391 return Ok(());
392 }
393
394 if let Some(response) = answer(&line, info, tools, &mut run) {
395 send(output, &response)?;
396 }
397 }
398}
399
400fn read_lines<T>(input: &mut impl BufRead, events: &Sender<T>, wrap: impl Fn(Input) -> T) {
402 loop {
403 let mut line = Vec::new();
404
405 match input.read_until(b'\n', &mut line) {
406 Ok(0) | Err(_) => break,
407 Ok(_) => {
408 if events.send(wrap(Input::Line(line))).is_err() {
409 return;
410 }
411 }
412 }
413 }
414
415 let _ = events.send(wrap(Input::Eof));
416}
417
418fn initialize(info: &ServerInfo, params: Option<&Value>) -> Value {
421 let asked = params
422 .and_then(|params| params.get("protocolVersion"))
423 .and_then(Value::as_str);
424
425 let version = asked
426 .filter(|asked| PROTOCOL_VERSIONS.contains(asked))
427 .unwrap_or(PROTOCOL_VERSIONS[0]);
428
429 let mut result = json!({
430 "protocolVersion": version,
431 "capabilities": { "tools": { "listChanged": false } },
432 "serverInfo": { "name": info.name, "version": info.version },
433 });
434
435 if let Some(instructions) = info.instructions {
436 result["instructions"] = Value::from(instructions);
437 }
438
439 result
440}
441
442fn call(id: Value, params: Option<&Value>, tools: &[Value]) -> Incoming {
444 let refuse = |id, message: String| Incoming::Reply(error(id, INVALID_PARAMS, message));
445
446 let Some(Value::Object(params)) = params else {
447 return refuse(id, String::from("Invalid params: not an object"));
448 };
449
450 let Some(name) = params.get("name").and_then(Value::as_str) else {
451 return refuse(id, String::from("Missing tool name"));
452 };
453
454 let Some(tool) = tools.iter().find(|tool| tool["name"] == name) else {
455 return refuse(id, format!("Unknown tool: {name}"));
456 };
457
458 let arguments = match params.get("arguments") {
459 None | Some(Value::Null) => Map::new(),
460 Some(Value::Object(arguments)) => arguments.clone(),
461 Some(_) => return refuse(id, String::from("Arguments must be an object")),
462 };
463
464 let schema = &tool["inputSchema"];
465
466 if schema["additionalProperties"] == false {
467 let properties = schema["properties"].as_object();
468 let declared = |key: &String| properties.is_some_and(|known| known.contains_key(key));
469
470 if let Some(key) = arguments.keys().find(|key| !declared(key)) {
471 let known = properties.into_iter().flat_map(|known| known.keys());
472 let err = unknown_argument(key, name, known.map(String::as_str));
473 let result = ToolResult::error::<NoErrorCode>(err);
474 return Incoming::Reply(json!({ "jsonrpc": "2.0", "id": id, "result": result }));
475 }
476 }
477
478 Incoming::Call(ToolCall {
479 id,
480 name: name.to_owned(),
481 arguments,
482 })
483}
484
485fn error(id: Value, code: i64, message: impl Into<String>) -> Value {
487 json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message.into() } })
488}
489
490fn is_false(value: &bool) -> bool {
491 !value
492}
493
494#[cfg(test)]
495mod tests {
496 use std::{fmt, sync::mpsc};
497
498 use anyhow::{Error, anyhow};
499 use schemars::JsonSchema;
500 use serde::Serialize;
501 use serde_json::{Value, json};
502
503 use crate::{
504 clap::commands::json_schema::output_schema,
505 error::PartialOutcome,
506 mcp::*,
507 printer::{CapturePrinter, Printer},
508 };
509
510 const INFO: ServerInfo = ServerInfo {
511 name: "app",
512 version: "1.2.3",
513 instructions: None,
514 };
515
516 fn tools() -> Vec<Value> {
517 vec![
518 json!({
519 "name": "item_get",
520 "inputSchema": {
521 "type": "object",
522 "properties": { "id": { "type": "string" }, "full": { "type": "boolean" } },
523 "required": ["id"],
524 "additionalProperties": false,
525 },
526 }),
527 json!({
528 "name": "item_list",
529 "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
530 }),
531 json!({ "name": "open", "inputSchema": { "type": "object" } }),
532 ]
533 }
534
535 fn reply(line: &str) -> Value {
536 match read(line.as_bytes(), &INFO, &tools()) {
537 Incoming::Reply(reply) => reply,
538 incoming => panic!("not a reply: {incoming:?}"),
539 }
540 }
541
542 fn code(reply: &Value) -> i64 {
543 reply["error"]["code"].as_i64().unwrap_or_default()
544 }
545
546 #[test]
547 fn a_message_that_is_not_json_is_a_parse_error() {
548 let reply = reply("{\"jsonrpc\": ");
549 assert_eq!(code(&reply), PARSE_ERROR, "{reply}");
550 assert_eq!(reply["id"], Value::Null);
551 assert_eq!(reply["jsonrpc"], "2.0");
552 }
553
554 #[test]
555 fn bytes_that_are_not_utf8_are_a_parse_error() {
556 let Incoming::Reply(reply) = read(b"{\"id\": \"\xff\"}\n", &INFO, &[]) else {
557 panic!("no reply");
558 };
559
560 assert_eq!(code(&reply), PARSE_ERROR, "{reply}");
561 assert_eq!(reply["id"], Value::Null);
562 }
563
564 #[test]
565 fn a_batch_or_a_scalar_is_an_invalid_request() {
566 for line in [
567 r#"[{"jsonrpc":"2.0","id":1,"method":"ping"}]"#,
568 "3",
569 "\"ping\"",
570 "null",
571 ] {
572 let reply = reply(line);
573 assert_eq!(code(&reply), INVALID_REQUEST, "{line}: {reply}");
574 assert_eq!(reply["id"], Value::Null, "{line}");
575 }
576 }
577
578 #[test]
579 fn an_invalid_request_keeps_its_id_when_valid() {
580 let wrong_version = reply(r#"{"jsonrpc":"1.0","id":7,"method":"ping"}"#);
581 assert_eq!(code(&wrong_version), INVALID_REQUEST);
582 assert_eq!(wrong_version["id"], 7);
583
584 let no_version = reply(r#"{"id":"a","method":"ping"}"#);
585 assert_eq!(code(&no_version), INVALID_REQUEST);
586 assert_eq!(no_version["id"], "a");
587
588 let response = reply(r#"{"jsonrpc":"2.0","id":8,"result":{}}"#);
589 assert_eq!(code(&response), INVALID_REQUEST);
590 assert_eq!(response["id"], 8);
591
592 let bad_id = reply(r#"{"jsonrpc":"2.0","id":{},"method":"ping"}"#);
593 assert_eq!(code(&bad_id), INVALID_REQUEST);
594 assert_eq!(bad_id["id"], Value::Null);
595 }
596
597 #[test]
598 fn a_notification_or_a_blank_line_gets_no_answer() {
599 for line in [
600 &br#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#[..],
601 br#"{"jsonrpc":"2.0","method":"unknown"}"#,
602 b" \r\n",
603 ] {
604 assert!(matches!(read(line, &INFO, &[]), Incoming::Ignore));
605 }
606 }
607
608 #[test]
609 fn an_unknown_method_is_not_found() {
610 let reply = reply(r#"{"jsonrpc":"2.0","id":1,"method":"resources/list"}"#);
611 assert_eq!(code(&reply), METHOD_NOT_FOUND, "{reply}");
612 assert_eq!(reply["id"], 1);
613 }
614
615 #[test]
616 fn ping_answers_an_empty_object() {
617 let reply = reply(r#"{"jsonrpc":"2.0","id":"p","method":"ping"}"#);
618 assert_eq!(reply, json!({ "jsonrpc": "2.0", "id": "p", "result": {} }));
619 }
620
621 #[test]
622 fn initialize_echoes_a_supported_version() {
623 let line = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}"#;
624 let reply = reply(line);
625
626 assert_eq!(
627 reply["result"],
628 json!({
629 "protocolVersion": "2025-06-18",
630 "capabilities": { "tools": { "listChanged": false } },
631 "serverInfo": { "name": "app", "version": "1.2.3" },
632 })
633 );
634 }
635
636 #[test]
637 fn initialize_answers_the_latest_version_to_an_unsupported_one() {
638 let info = ServerInfo {
639 instructions: Some("Call item_list first."),
640 ..INFO
641 };
642
643 for params in [r#"{"protocolVersion":"2099-01-01"}"#, "{}", "null"] {
644 let line =
645 format!(r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{params}}}"#);
646 let Incoming::Reply(reply) = read(line.as_bytes(), &info, &[]) else {
647 panic!("no reply");
648 };
649
650 assert_eq!(reply["result"]["protocolVersion"], PROTOCOL_VERSIONS[0]);
651 assert_eq!(reply["result"]["instructions"], "Call item_list first.");
652 }
653 }
654
655 #[test]
656 fn tools_list_answers_the_tools_given() {
657 let reply = reply(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#);
658 assert_eq!(reply["result"]["tools"], Value::from(tools()));
659 }
660
661 #[test]
662 fn a_call_with_bad_params_is_refused() {
663 for params in [
664 "",
665 r#","params":3"#,
666 r#","params":{}"#,
667 r#","params":{"name":"item_put"}"#,
668 r#","params":{"name":"item_get","arguments":[]}"#,
669 ] {
670 let line = format!(r#"{{"jsonrpc":"2.0","id":4,"method":"tools/call"{params}}}"#);
671 let reply = reply(&line);
672 assert_eq!(code(&reply), INVALID_PARAMS, "{line}: {reply}");
673 assert_eq!(reply["id"], 4);
674 }
675 }
676
677 #[test]
678 fn a_checked_call_is_handed_over() {
679 let line = r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"item_get","arguments":{"id":"x"}}}"#;
680 let Incoming::Call(call) = read(line.as_bytes(), &INFO, &tools()) else {
681 panic!("no call");
682 };
683
684 assert_eq!(call.id, 5);
685 assert_eq!(call.name, "item_get");
686 assert_eq!(Value::from(call.arguments.clone()), json!({ "id": "x" }));
687
688 let line = r#"{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"open","arguments":{"any":1}}}"#;
689 assert!(matches!(
690 read(line.as_bytes(), &INFO, &tools()),
691 Incoming::Call(_)
692 ));
693
694 let line = r#"{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"item_list","arguments":null}}"#;
695 let Incoming::Call(call) = read(line.as_bytes(), &INFO, &tools()) else {
696 panic!("no call");
697 };
698 assert!(call.arguments.is_empty());
699 }
700
701 #[test]
702 fn an_undeclared_argument_is_a_failed_result_naming_the_declared_ones() {
703 let line = r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"item_get","arguments":{"di":"x"}}}"#;
704 let unknown = reply(line);
705 let result = &unknown["result"];
706
707 assert_eq!(unknown["id"], 5);
708 assert_eq!(result["isError"], true, "{unknown}");
709
710 let report: Value =
711 serde_json::from_str(result["content"][0]["text"].as_str().unwrap()).unwrap();
712 assert_eq!(
713 report["error"],
714 "Unknown argument `di`, item_get takes `full`, `id`"
715 );
716 assert_eq!(report["code"], "usage-invalid", "{report}");
717
718 let line = r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"item_list","arguments":{"x":1}}}"#;
719 let none = reply(line);
720 assert!(none.to_string().contains("item_list takes none"), "{none}");
721 }
722
723 #[derive(Serialize, JsonSchema)]
724 struct Item {
725 id: u32,
726 }
727
728 impl fmt::Display for Item {
729 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730 writeln!(f, "{}", self.id)
731 }
732 }
733
734 fn printed(values: &[u32], events: &[&str], result: Result<(), Error>) -> Value {
735 let mut printer = CapturePrinter::default();
736
737 for event in events {
738 printer.event(json!({ "event": event })).unwrap();
739 }
740
741 for id in values {
742 printer.out(Item { id: *id }).unwrap();
743 }
744
745 serde_json::to_value(ToolResult::printed::<NoErrorCode>(printer, result)).unwrap()
746 }
747
748 #[test]
749 fn a_printed_value_is_structured_after_its_events() {
750 assert_eq!(
751 printed(&[1], &["started"], Ok(())),
752 json!({
753 "content": [
754 { "type": "text", "text": r#"{"event":"started"}"# },
755 { "type": "text", "text": r#"{"id":1}"# },
756 ],
757 "structuredContent": { "id": 1 },
758 })
759 );
760 }
761
762 #[test]
763 fn a_partial_outcome_keeps_its_result_as_an_error() {
764 let result = printed(&[1], &[], Err(PartialOutcome::default().into()));
765
766 assert_eq!(result["structuredContent"], json!({ "id": 1 }));
767 assert_eq!(result["isError"], true);
768 }
769
770 #[test]
771 fn a_failure_or_a_wrong_count_is_a_failure_object() {
772 for (values, outcome) in [
773 (&[][..], Ok(())),
774 (&[1, 2][..], Ok(())),
775 (&[1][..], Err(anyhow!("Boom"))),
776 ] {
777 let expected = match &outcome {
778 Ok(()) => format!("The command printed {} values, one expected", values.len()),
779 Err(err) => err.to_string(),
780 };
781
782 let result = printed(values, &["started"], outcome);
783 assert_eq!(result["isError"], true, "{result}");
784 assert!(result.get("structuredContent").is_none(), "{result}");
785 assert_eq!(result["content"].as_array().unwrap().len(), 1, "{result}");
786
787 let text = result["content"][0]["text"].as_str().unwrap();
788 let report: Value = serde_json::from_str(text).unwrap();
789 assert_eq!(report["error"], expected);
790 assert_eq!(report["code"], Value::Null);
791 }
792 }
793
794 #[test]
795 fn a_value_that_is_not_an_object_is_text_alone() {
796 let result = serde_json::to_value(ToolResult::success(json!([1]))).unwrap();
797 assert_eq!(
798 result,
799 json!({ "content": [{ "type": "text", "text": "[1]" }] })
800 );
801 }
802
803 #[test]
804 fn an_output_schema_needs_an_object_root() {
805 let object = json!({ "type": "object", "properties": {} });
806 assert_eq!(tool_output_schema(&object), Some(&object));
807
808 for schema in [
809 json!({ "$ref": "#/$defs/A", "$defs": { "A": { "type": "object" } } }),
810 json!({ "anyOf": [{ "type": "object" }, { "type": "object" }] }),
811 json!({ "oneOf": [{ "type": "object" }] }),
812 json!({ "type": "array" }),
813 json!({ "type": ["object", "null"] }),
814 ] {
815 assert_eq!(tool_output_schema(&schema), None, "{schema}");
816 }
817
818 let schema = output_schema::<Item>();
819 assert!(tool_output_schema(&schema).is_some(), "{schema}");
820 }
821
822 #[test]
823 fn the_loop_answers_each_line_and_runs_each_call() {
824 let input = concat!(
825 r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#,
826 "\n\n",
827 r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
828 "\n",
829 r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"item_get","arguments":{"id":"x"}}}"#,
830 "\n\u{0}\n",
831 r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"item_list"}}"#,
832 );
833
834 let mut output = Vec::new();
835 let mut calls = Vec::new();
836
837 serve_on(
838 &mut input.as_bytes(),
839 &mut output,
840 &INFO,
841 &tools(),
842 |call| {
843 calls.push(call.name.clone());
844 ToolResult::success(json!({ "called": call.name }))
845 },
846 )
847 .unwrap();
848
849 let output = String::from_utf8(output).unwrap();
850 let replies: Vec<Value> = output
851 .lines()
852 .map(|line| serde_json::from_str(line).unwrap())
853 .collect();
854
855 assert_eq!(calls, ["item_get", "item_list"]);
856 assert_eq!(replies.len(), 4, "{output}");
857 assert_eq!(replies[0]["id"], 1);
858 assert_eq!(
859 replies[1]["result"]["structuredContent"]["called"],
860 "item_get"
861 );
862 assert_eq!(code(&replies[2]), PARSE_ERROR);
863 assert_eq!(replies[3]["id"], 3);
864 }
865
866 #[test]
867 fn the_reader_sends_each_line_then_eof() {
868 let (events, inbox) = mpsc::channel();
869 read_lines(&mut &b"a\n\xff\nb"[..], &events, |input| input);
870 drop(events);
871
872 let lines: Vec<Option<Vec<u8>>> = inbox
873 .iter()
874 .map(|input| match input {
875 Input::Line(line) => Some(line),
876 Input::Eof => None,
877 })
878 .collect();
879
880 assert_eq!(
881 lines,
882 [
883 Some(b"a\n".to_vec()),
884 Some(b"\xff\n".to_vec()),
885 Some(b"b".to_vec()),
886 None
887 ]
888 );
889 }
890
891 #[test]
892 fn a_response_carries_the_call_id() {
893 let call = ToolCall {
894 id: json!("c1"),
895 name: String::from("item_get"),
896 arguments: Map::new(),
897 };
898
899 let response = call.respond(ToolResult::success(json!({ "id": 1 })));
900 assert_eq!(response["jsonrpc"], "2.0");
901 assert_eq!(response["id"], "c1");
902 assert_eq!(response["result"]["structuredContent"], json!({ "id": 1 }));
903 }
904}