44import json
55import shlex
66import contextlib
7+ import os
8+ import subprocess
9+ from pathlib import Path
710from typing import Any
811
912from ..protocols .mcp_tools import build_tool_catalogue
1013
1114
1215def get_agent_tools () -> list [dict [str , Any ]]:
13- """Convert MCP tools into OpenAI-compatible tool schemas for the agent."""
16+ """Convert MCP tools and OS tools into OpenAI-compatible tool schemas for the agent."""
1417 mcp_tools = build_tool_catalogue ()
1518 tools = []
19+
20+ # OS Level Tools
21+ tools .extend ([
22+ {
23+ "type" : "function" ,
24+ "function" : {
25+ "name" : "read_file" ,
26+ "description" : "Read the contents of a file." ,
27+ "parameters" : {
28+ "type" : "object" ,
29+ "properties" : {
30+ "path" : {"type" : "string" , "description" : "Path to the file to read." }
31+ },
32+ "required" : ["path" ],
33+ "additionalProperties" : False ,
34+ }
35+ }
36+ },
37+ {
38+ "type" : "function" ,
39+ "function" : {
40+ "name" : "write_file" ,
41+ "description" : "Write or overwrite the contents of a file." ,
42+ "parameters" : {
43+ "type" : "object" ,
44+ "properties" : {
45+ "path" : {"type" : "string" , "description" : "Path to the file." },
46+ "content" : {"type" : "string" , "description" : "Content to write." }
47+ },
48+ "required" : ["path" , "content" ],
49+ "additionalProperties" : False ,
50+ }
51+ }
52+ },
53+ {
54+ "type" : "function" ,
55+ "function" : {
56+ "name" : "run_command" ,
57+ "description" : "Run a shell command in the project directory." ,
58+ "parameters" : {
59+ "type" : "object" ,
60+ "properties" : {
61+ "command" : {"type" : "string" , "description" : "The shell command to execute." }
62+ },
63+ "required" : ["command" ],
64+ "additionalProperties" : False ,
65+ }
66+ }
67+ },
68+ {
69+ "type" : "function" ,
70+ "function" : {
71+ "name" : "list_dir" ,
72+ "description" : "List the contents of a directory." ,
73+ "parameters" : {
74+ "type" : "object" ,
75+ "properties" : {
76+ "path" : {"type" : "string" , "description" : "Path to the directory." }
77+ },
78+ "required" : ["path" ],
79+ "additionalProperties" : False ,
80+ }
81+ }
82+ }
83+ ])
84+
1685 for mcp_tool in mcp_tools :
17- # Some providers prefer parameters to be complete JSON schema
1886 parameters = dict (mcp_tool .input_schema )
19- # Ensure additionalProperties is False if required for strict structured output
20- # But we leave it as is from mcp_tools.py which already sets it.
2187 tools .append ({
2288 "type" : "function" ,
2389 "function" : {
24- "name" : mcp_tool .name .replace ("." , "_" ), # e.g. mythic_vibe.memory_search -> mythic_vibe_memory_search
90+ "name" : mcp_tool .name .replace ("." , "_" ),
2591 "description" : mcp_tool .description ,
2692 "parameters" : parameters ,
2793 }
2894 })
2995 return tools
3096
3197
32- def execute_tool (name : str , arguments : dict [str , Any ]) -> str :
98+ def execute_tool (name : str , arguments : dict [str , Any ], project_root : Path | None = None ) -> str :
3399 """Execute a tool request and return its stdout/stderr as a string."""
100+
101+ root_path = project_root if project_root is not None else Path .cwd ()
102+
103+ if name == "read_file" :
104+ path = root_path / arguments .get ("path" , "" )
105+ try :
106+ return path .read_text (encoding = "utf-8" )
107+ except Exception as exc :
108+ return f"Failed to read file: { exc } "
109+
110+ if name == "write_file" :
111+ path = root_path / arguments .get ("path" , "" )
112+ try :
113+ path .parent .mkdir (parents = True , exist_ok = True )
114+ path .write_text (arguments .get ("content" , "" ), encoding = "utf-8" )
115+ return f"Successfully wrote to { path } "
116+ except Exception as exc :
117+ return f"Failed to write file: { exc } "
118+
119+ if name == "run_command" :
120+ command = arguments .get ("command" , "" )
121+ try :
122+ result = subprocess .run (
123+ command , shell = True , cwd = str (root_path ),
124+ stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True
125+ )
126+ return result .stdout or f"Command executed with exit code { result .returncode } "
127+ except Exception as exc :
128+ return f"Command failed: { exc } "
129+
130+ if name == "list_dir" :
131+ path = root_path / arguments .get ("path" , "." )
132+ try :
133+ items = os .listdir (path )
134+ return "\\ n" .join (sorted (items ))
135+ except Exception as exc :
136+ return f"Failed to list directory: { exc } "
137+
34138 from ..app import main
35139
36- # Tools are prefixed with mythic_vibe_
37140 if not name .startswith ("mythic_vibe_" ):
38141 return f"Error: Unknown tool prefix { name } "
39142
40- # Extract the actual command name
41143 command_name = name [len ("mythic_vibe_" ):]
42-
43- # argv array from the tool arguments
44144 tool_argv = arguments .get ("argv" , [])
45145 if not isinstance (tool_argv , list ):
46146 return f"Error: argv must be a list, got { type (tool_argv )} "
47147
48- # Ensure they are strings
49148 tool_argv = [str (arg ) for arg in tool_argv ]
50-
51149 full_argv = [command_name ] + tool_argv
52150
53- # Capture output
54151 stdout = io .StringIO ()
55152 stderr = io .StringIO ()
56153
57154 try :
58155 with contextlib .redirect_stdout (stdout ), contextlib .redirect_stderr (stderr ):
59156 exit_code = main (full_argv )
60157 except SystemExit as exc :
61- # Argparse might call sys.exit, we catch it so the shell doesn't die.
62158 exit_code = exc .code if exc .code is not None else 0
63159 except Exception as exc :
64- return f"Tool execution crashed: { exc } \n { stderr .getvalue ()} "
160+ return f"Tool execution crashed: { exc } \\ n{ stderr .getvalue ()} "
65161
66162 output = stdout .getvalue ()
67163 err_output = stderr .getvalue ()
@@ -70,7 +166,7 @@ def execute_tool(name: str, arguments: dict[str, Any]) -> str:
70166 if output :
71167 result .append (output .strip ())
72168 if err_output :
73- result .append (f"STDERR:\n { err_output .strip ()} " )
169+ result .append (f"STDERR:\\ n{ err_output .strip ()} " )
74170 result .append (f"Exit code: { exit_code } " )
75171
76- return "\n " .join (result )
172+ return "\\ n" .join (result )
0 commit comments