From 60219ef8f8e78db063bf105a9e1e052dfd8bae4e Mon Sep 17 00:00:00 2001 From: fabolous005 Date: Thu, 5 Nov 2026 10:17:18 +0100 Subject: [PATCH] shellmcp: tool description lists installed modern tools --- shellmcp.py | 25 +++++++++++++++++++++++++ test_shellmcp.py | 11 +++++++++++ 2 files changed, 36 insertions(+) diff --git a/shellmcp.py b/shellmcp.py index 6a599c4..98edcbe 100644 --- a/shellmcp.py +++ b/shellmcp.py @@ -56,3 +56,28 @@ def sandbox(cwd, hide=()): elif os.path.exists(path): argv += ["--ro-bind", "/dev/null", path] return argv + ["--chdir", cwd] + +PREFERRED = { # named in the tool description when installed, in this order + "rg": "search file contents (instead of grep)", "fd": "find files (instead of find)", + "eza": "list files (instead of ls)", "sd": "find and replace (instead of sed)", + "dust": "disk usage (instead of du)", "procs": "processes (instead of ps)", + "jq": "query JSON", "ast-grep": "search code by syntax", "tokei": "count lines of code"} + + +def describe(mode, cwd, timeout, limit): + """The tool description: where commands run, the limits, and the modern tools installed.""" + text = (f"Run a bash command on {socket.gethostname()} as {getpass.getuser()} in {cwd}. " + f"Returns its exit code and combined stdout and stderr (at most {limit} characters; " + f"the middle of longer output is cut). Commands are killed after {timeout} s. " + "There is no terminal: interactive programs, pagers and prompts fail.") + if mode == "readonly": + text += (" Read-only sandbox: nothing can be written except /tmp, which is emptied " + "after every command; no network; other processes are not visible. To change " + "something, give the user the command to run instead.") + else: + text += (" Commands run with the user's full permissions: they can change files, " + "processes and the network.") + tools = [f"{name}: {what}" for name, what in PREFERRED.items() if shutil.which(name)] + if tools: + text += " Installed, prefer these: " + "; ".join(tools) + "." + return text diff --git a/test_shellmcp.py b/test_shellmcp.py index 9fa972e..d3c5771 100644 --- a/test_shellmcp.py +++ b/test_shellmcp.py @@ -44,3 +44,14 @@ class Sandbox(unittest.TestCase): self.assertNotIn("key", out) self.assertIn("tmp-ok", out) self.assertFalse(os.path.exists(os.path.join(d, "new"))) + +class Describe(unittest.TestCase): + def test_names_only_installed_tools_and_the_mode(self): + with mock.patch("shutil.which", lambda name: name in ("fd", "rg")): + text = shellmcp.describe("readonly", "/srv", 60, 8000) + self.assertIn("in /srv.", text) + self.assertIn("Read-only sandbox", text) + self.assertIn("prefer these: rg: search file contents (instead of grep); " + "fd: find files (instead of find).", text) + self.assertNotIn("jq", text) + self.assertIn("full permissions", shellmcp.describe("all", "/srv", 60, 8000))