22
33import asyncio
44import json
5- import shlex
65
76from fastmcp .utilities .logging import get_logger
87from pydantic import BaseModel
@@ -102,60 +101,65 @@ async def execute_cli_command(
102101 flags: Dictionary of flag names and values
103102 output_format: Output format ('json', 'table', 'yaml')
104103 timeout: Command timeout in seconds
105- auto_parse: If True, automatically parse JSON output
104+ auto_parse: Whether to automatically parse JSON output
106105
107106 Returns:
108- CLIResult if auto_parse=False, parsed output if auto_parse=True
107+ CLIResult if auto_parse=False, parsed dict/str if auto_parse=True
109108
110109 Raises:
111110 CycloidCLIError: If command execution fails
112111 """
113112 cmd_parts = self ._build_command (subcommand , args , flags , output_format )
114- command = " " .join (shlex .quote (part ) for part in cmd_parts )
115-
116- logger .debug ("Executing Cycloid CLI command" , extra = {"command" : command })
113+ command = " " .join (cmd_parts )
117114
118115 try :
119116 stdout , stderr , exit_code = await self ._execute_command (cmd_parts , timeout )
120117
121118 result = CLIResult (
122119 success = exit_code == 0 ,
123- stdout = stdout .decode ("utf-8" ). strip () ,
124- stderr = stderr .decode ("utf-8" ). strip () ,
120+ stdout = stdout .decode () if stdout else "" ,
121+ stderr = stderr .decode () if stderr else "" ,
125122 exit_code = exit_code ,
126123 command = command ,
127124 )
128125
129126 if not result .success :
130127 logger .error (
131- f"CLI command failed: { result . stderr } " ,
128+ f"CLI command failed with exit code { exit_code } " ,
132129 extra = {
133130 "command" : command ,
134- "exit_code" : result . exit_code ,
131+ "exit_code" : exit_code ,
135132 "stderr" : result .stderr ,
136133 },
137134 )
138135 raise CycloidCLIError (
139136 f"CLI command failed: { result .stderr } " ,
140137 command = command ,
141- exit_code = result . exit_code ,
138+ exit_code = exit_code ,
142139 stderr = result .stderr ,
143140 )
144141
145- # Auto-parse if requested
146- if auto_parse and output_format == "json" :
147- try :
148- return json .loads (result .stdout )
149- except json .JSONDecodeError as e :
150- logger .error (f"Failed to parse CLI JSON output: { str (e )} " )
151- raise CycloidCLIError (
152- f"Failed to parse CLI JSON output: { str (e )} " ,
153- command = result .command ,
154- exit_code = result .exit_code ,
155- stderr = result .stderr ,
156- )
157- elif auto_parse :
158- return result .stdout
142+ if auto_parse :
143+ if output_format == "json" :
144+ try :
145+ return json .loads (result .stdout )
146+ except json .JSONDecodeError as e :
147+ logger .error (
148+ f"Failed to parse JSON output: { str (e )} " ,
149+ extra = {
150+ "command" : command ,
151+ "stdout" : result .stdout ,
152+ "error" : str (e ),
153+ },
154+ )
155+ raise CycloidCLIError (
156+ f"Failed to parse JSON output: { str (e )} " ,
157+ command = command ,
158+ exit_code = exit_code ,
159+ stderr = f"JSON parse error: { str (e )} " ,
160+ )
161+ else :
162+ return result .stdout
159163
160164 return result
161165
@@ -189,7 +193,7 @@ async def execute_cli(
189193 flags : Optional [Dict [str , Union [str , bool ]]] = None ,
190194 output_format : str = "json" ,
191195 timeout : int = 30 ,
192- ) -> Union [Dict [str , Any ], str ]:
196+ ) -> Union [Dict [str , Any ], List [ Dict [ str , Any ]], str ]:
193197 """
194198 Execute a Cycloid CLI command with automatic output parsing.
195199
@@ -201,20 +205,40 @@ async def execute_cli(
201205 timeout: Command timeout in seconds
202206
203207 Returns:
204- Parsed output - JSON dict for 'json' format, string for others
208+ Parsed output - JSON dict/list for 'json' format, string for others
205209
206210 Raises:
207211 CycloidCLIError: If command execution fails or parsing fails
208212 """
209213 result = await self .execute_cli_command (
210- subcommand , args , flags , output_format , timeout , auto_parse = True
214+ subcommand , args , flags , output_format , timeout , auto_parse = False
211215 )
212- # Type guard to ensure we return the expected types
213- if isinstance (result , (dict , str )):
214- return result
215- else :
216- # This shouldn't happen with auto_parse=True, but handle it gracefully
217- return str (result )
216+
217+ # Parse the result if it's a CLIResult
218+ if isinstance (result , CLIResult ):
219+ if output_format == "json" :
220+ try :
221+ return self .parse_cli_output (result .stdout )
222+ except ValueError as e :
223+ logger .error (
224+ f"Failed to parse CLI JSON output: { str (e )} " ,
225+ extra = {
226+ "command" : result .command ,
227+ "stdout" : result .stdout ,
228+ "error" : str (e ),
229+ },
230+ )
231+ raise CycloidCLIError (
232+ f"Failed to parse CLI JSON output: { str (e )} " ,
233+ command = result .command ,
234+ exit_code = result .exit_code ,
235+ stderr = result .stderr ,
236+ )
237+ else :
238+ return result .stdout
239+
240+ # If auto_parse was already done, return as-is
241+ return result
218242
219243 @staticmethod
220244 def process_cli_response (
@@ -254,3 +278,37 @@ def process_cli_response(
254278 return default
255279 else :
256280 return default
281+
282+ @staticmethod
283+ def parse_cli_output (
284+ output : Union [str , Dict [str , Any ], List [Dict [str , Any ]]]
285+ ) -> Union [Dict [str , Any ], List [Dict [str , Any ]]]:
286+ """
287+ Parse CLI output that may be in JSON or Python literal format.
288+
289+ Args:
290+ output: Raw CLI output (string, dict, or list)
291+
292+ Returns:
293+ Parsed output as dict or list
294+
295+ Raises:
296+ ValueError: If parsing fails completely
297+ """
298+ # If already parsed, return as-is
299+ if isinstance (output , (dict , list )):
300+ return output
301+
302+ # Try to parse as JSON first
303+ try :
304+ return json .loads (output )
305+ except json .JSONDecodeError :
306+ # If JSON parsing fails, try to evaluate as Python literal
307+ try :
308+ import ast
309+ return ast .literal_eval (output )
310+ except (ValueError , SyntaxError ):
311+ # If both fail, raise an error
312+ raise ValueError (
313+ f"Failed to parse CLI output as JSON or Python literal: { output [:100 ]} ..."
314+ )
0 commit comments