22import logging
33import sys
44import traceback
5+ import uuid
56from typing import Optional , Dict , Annotated
67
78import aiofiles
@@ -28,10 +29,12 @@ class RunCommandConfirmation(BaseModel):
2829class RunUnixCommand (BaseModel ):
2930 command : str = Field (description = "The command that was run" )
3031 return_code : int = Field (description = "Return code of command, 0 usually means successful" )
32+ output_file : Optional [str ] = Field (description = "Output file location containing all of standard out" )
3133 output : str = Field (description = "Output of command as string" )
32- output_truncated : bool = Field (description = "If true, output is truncated" )
34+ output_truncated : bool = Field (description = "If true, output string is truncated" )
35+ error_file : Optional [str ] = Field (description = "Error file location containing all of standard error" )
3336 error : str = Field (description = "Output of stderr, could be errors or progress information" )
34- error_truncated : bool = Field (description = "If true, error is truncated" )
37+ error_truncated : bool = Field (description = "If true, error string is truncated" )
3538 notes : Optional [str ] = Field (description = "Notes for understanding the command output or fixing failed commands" )
3639
3740
@@ -85,9 +88,25 @@ async def run_unix_command(
8588 output_length_limit = output_length_limit )
8689 server_ctx = await get_server_context ()
8790
91+ try :
92+ assert_elicitation (server_ctx )
93+ confirm_result = await ctx .elicit (
94+ message = f"{ command } \n Should I run this command?" ,
95+ schema = RunCommandConfirmation )
96+ match confirm_result :
97+ case AcceptedElicitation (data = data ):
98+ if not data .confirm :
99+ return None
100+ case DeclinedElicitation ():
101+ return None
102+ case CancelledElicitation ():
103+ return None
104+ except McpError :
105+ logger .warning ("elicit not supported, continuing" )
106+
88107 try :
89108 result = await _run_unix_command (ctx , command = command , additional_hosts = additional_hosts , env = env ,
90- output_length_limit = output_length_limit )
109+ output_length_limit = output_length_limit , capture_output_to_file = True )
91110
92111 if result .return_code != 0 and (
93112 "executable file not found" in result .error or "command not found" in result .error ):
@@ -110,8 +129,10 @@ async def run_unix_command(
110129 return RunUnixCommand (
111130 command = command ,
112131 return_code = - 1 ,
132+ output_file = None ,
113133 output = "" ,
114134 output_truncated = False ,
135+ error_file = None ,
115136 error = '' .join (traceback .format_exception (exc_type , exc_value , exc_tb )),
116137 error_truncated = False ,
117138 notes = None ,
@@ -138,8 +159,8 @@ async def _run_unix_command(
138159 stdin : Optional [str ] = None ,
139160 env : Optional [Dict [str , str ]] = None ,
140161 output_length_limit : Optional [int ] = None ,
141- ) -> Optional [
142- RunUnixCommand ]:
162+ capture_output_to_file : bool = False ,
163+ ) -> Optional [ RunUnixCommand ]:
143164 command = command .strip ()
144165 if not command :
145166 raise McpError (ErrorData (code = INVALID_REQUEST , message = "command required" ))
@@ -150,30 +171,23 @@ async def _run_unix_command(
150171
151172 stdin_bytes = stdin .encode ("utf-8" ) if stdin else None
152173
153- try :
154- assert_elicitation (server_ctx )
155- confirm_result = await ctx .elicit (
156- message = f"{ command } \n Should I run this command?" ,
157- schema = RunCommandConfirmation )
158- match confirm_result :
159- case AcceptedElicitation (data = data ):
160- if not data .confirm :
161- return None
162- case DeclinedElicitation ():
163- return None
164- case CancelledElicitation ():
165- return None
166- except McpError :
167- logger .warning ("elicit not supported, continuing" )
168-
169174 output_limiter = OutputLimiter (output_length_limit )
170175 error_limiter = OutputLimiter (output_length_limit )
171176
177+ if capture_output_to_file :
178+ capture_basename = uuid .uuid4 ().hex
179+ capture_output_file = capture_basename + ".out"
180+ capture_error_file = capture_basename + ".err"
181+ else :
182+ capture_output_file = None
183+ capture_error_file = None
184+
172185 async with aiofiles .tempfile .TemporaryFile (mode = "w+b" ) as stdout_file :
173186 async with aiofiles .tempfile .TemporaryFile (mode = "w+b" ) as stderr_file :
174187 # Use a common working directory for the session to chain together commands
175188 work_path = ctx .request_context .lifespan_context .work_path
176189 docker_command = ["docker" , "run" , "--rm" ]
190+
177191 if server_ctx .open_world :
178192 docker_command .extend ([
179193 "--cap-add" , "NET_BIND_SERVICE" ,
@@ -187,6 +201,12 @@ async def _run_unix_command(
187201 "--cap-drop" , "NET_RAW" ,
188202 "--network" , "none" ,
189203 ])
204+
205+ if capture_output_file :
206+ docker_command .extend (["-e" , f"STDOUT_LOG={ capture_output_file } " ])
207+ if capture_error_file :
208+ docker_command .extend (["-e" , f"STDERR_LOG={ capture_error_file } " ])
209+
190210 docker_command .extend ([
191211 "-v" , f"{ server_ctx .mcp_session_volume } :/work" ,
192212 "-v" , f"{ server_ctx .seclists_volume } :/usr/share/seclists" ,
@@ -266,8 +286,10 @@ async def _run_unix_command(
266286 return RunUnixCommand (
267287 command = command ,
268288 return_code = return_code ,
289+ output_file = capture_output_file ,
269290 output = output ,
270291 output_truncated = output_truncated ,
292+ error_file = capture_error_file ,
271293 error = "" ,
272294 error_truncated = False ,
273295 notes = None ,
@@ -278,8 +300,10 @@ async def _run_unix_command(
278300 return RunUnixCommand (
279301 command = command ,
280302 return_code = return_code ,
303+ output_file = capture_output_file ,
281304 output = output ,
282305 output_truncated = output_truncated ,
306+ error_file = capture_error_file ,
283307 error = error_tail ,
284308 error_truncated = error_truncated ,
285309 notes = open_world_command_disable_notes if not server_ctx .open_world else None ,
0 commit comments