@@ -35,14 +35,15 @@ pub fn list(tools: &mut Tools) {
3535pub struct SpawnRequest {
3636 program : String ,
3737 arguments : Vec < String > ,
38- stdin : String ,
38+ #[ serde( flatten) ]
39+ interact : InteractRequest ,
3940}
4041
4142/// Spawns a process with input and output interaction.
4243///
4344/// To interact with the process, use the `exec_interact` tool. To kill the program, use the
44- /// `exec_kill` tool. The initial interaction is built-in by taking the same `stdin` field as
45- /// `exec_interact` and returning the same fields.
45+ /// `exec_kill` tool. The initial interaction is built-in by taking the same `stdin` and
46+ /// `close_stdin` fields as ` exec_interact` and returning the same fields.
4647///
4748/// This tool can be used together with `file_write` (setting `executable` to create an executable
4849/// file) to execute a Python or shell script.
@@ -53,7 +54,7 @@ async fn _spawn(_: Parameters<SpawnRequest>) -> Result<Json<InteractResponse>, S
5354}
5455
5556async fn spawn ( params : Parameters < SpawnRequest > ) -> Result < Json < InteractResponse > , String > {
56- let SpawnRequest { program, arguments, stdin } = params. 0 ;
57+ let SpawnRequest { program, arguments, interact : request } = params. 0 ;
5758 let mut child = Command :: new ( program)
5859 . args ( arguments)
5960 . stdin ( Stdio :: piped ( ) )
@@ -62,18 +63,21 @@ async fn spawn(params: Parameters<SpawnRequest>) -> Result<Json<InteractResponse
6263 . spawn ( )
6364 . map_err ( |e| e. to_string ( ) ) ?;
6465 let running = Running {
65- stdin : child. stdin . take ( ) . unwrap ( ) ,
66+ stdin : Some ( child. stdin . take ( ) . unwrap ( ) ) ,
6667 stdout : child. stdout . take ( ) . unwrap ( ) ,
6768 stderr : child. stderr . take ( ) . unwrap ( ) ,
6869 child,
6970 } ;
7071 * STATE . lock ( ) . unwrap ( ) = Some ( running) ;
71- interact ( Parameters ( InteractRequest { stdin } ) ) . await
72+ interact ( Parameters ( request ) ) . await
7273}
7374
7475#[ derive( Serialize , Deserialize , JsonSchema ) ]
7576pub struct InteractRequest {
77+ #[ serde( default ) ]
7678 stdin : String ,
79+ #[ serde( default ) ]
80+ close_stdin : bool ,
7781}
7882
7983#[ derive( Serialize , Deserialize , JsonSchema ) ]
@@ -87,12 +91,18 @@ pub struct InteractResponse {
8791/// Interacts with a running process in textual form.
8892///
8993/// The `stdin` parameter will be written to the standard input of the process. It can be empty if
90- /// interacting for output only. In the response, the `running` field indicates whether the process
91- /// is still running. When it's not running anymore, the `success` field indicates if it terminated
92- /// successfully or not. The `stdout` and `stderr` fields contain the output read from the standard
93- /// output and error since the last interaction. The tool will listen for output until some amount
94- /// of inactivity or some fixed deadline, whichever happens first. The tool will fail if the process
95- /// outputs binary data that is not UTF-8.
94+ /// interacting for output only. When the `close_stdin` parameter is set, the standard input will be
95+ /// closed. This is necessary for some programs to start processing. Note that once the standard
96+ /// input is closed, future interactions must not set `stdin` to non-empty content.
97+ ///
98+ /// In the response, the `running` field indicates whether the process is still running. When it's
99+ /// not running anymore, the `success` field indicates if it terminated successfully or not. The
100+ /// `stdout` and `stderr` fields contain the output read from the standard output and error since
101+ /// the last interaction.
102+ ///
103+ /// The tool will listen for output until some amount of inactivity or some fixed deadline,
104+ /// whichever happens first. The tool will fail if the process outputs binary data that is not
105+ /// UTF-8.
96106#[ rmcp:: tool( name = "exec_interact" ) ]
97107async fn _interact ( _: Parameters < InteractRequest > ) -> Result < Json < InteractResponse > , String > {
98108 // TODO(https://github.com/modelcontextprotocol/rust-sdk/issues/495): Remove when fixed.
@@ -101,13 +111,20 @@ async fn _interact(_: Parameters<InteractRequest>) -> Result<Json<InteractRespon
101111
102112#[ allow( clippy:: await_holding_lock) ]
103113async fn interact ( params : Parameters < InteractRequest > ) -> Result < Json < InteractResponse > , String > {
104- let InteractRequest { stdin } = params. 0 ;
114+ let InteractRequest { stdin, close_stdin } = params. 0 ;
105115 let mut state = STATE . lock ( ) . unwrap ( ) ;
106116 let Some ( running) = & mut * state else {
107117 return Err ( "no running process" . to_string ( ) ) ;
108118 } ;
109119 if !stdin. is_empty ( ) {
110- running. stdin . write_all ( stdin. as_bytes ( ) ) . await . map_err ( |e| e. to_string ( ) ) ?;
120+ let Some ( pipe) = running. stdin . as_mut ( ) else { return Err ( "stdin is closed" . to_string ( ) ) } ;
121+ pipe. write_all ( stdin. as_bytes ( ) ) . await . map_err ( |e| e. to_string ( ) ) ?;
122+ }
123+ if close_stdin {
124+ let Some ( mut pipe) = running. stdin . take ( ) else {
125+ return Err ( "stdin is already closed" . to_string ( ) ) ;
126+ } ;
127+ pipe. shutdown ( ) . await . map_err ( |e| e. to_string ( ) ) ?;
111128 }
112129 const SIZE : usize = 1024 ;
113130 let mut stdout = vec ! [ 0 ; SIZE ] ;
@@ -179,7 +196,7 @@ async fn kill(params: Parameters<KillRequest>) -> Result<Json<String>, String> {
179196static STATE : Mutex < Option < Running > > = Mutex :: new ( None ) ;
180197
181198struct Running {
182- stdin : ChildStdin ,
199+ stdin : Option < ChildStdin > ,
183200 stdout : ChildStdout ,
184201 stderr : ChildStderr ,
185202 child : Child ,
0 commit comments