From 51d9d28b53f1c553d3b7e07dca52a1abd05efd92 Mon Sep 17 00:00:00 2001 From: Ihor Radchenko Date: Sat, 18 Apr 2026 14:39:45 +0200 Subject: [PATCH 1/7] Avoid too long search results * macher.el (macher--tool-search-helper): Respect `macher--max-read-length' when returning search results. --- macher.el | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/macher.el b/macher.el index 0d47175..87de205 100644 --- a/macher.el +++ b/macher.el @@ -3077,6 +3077,11 @@ piping the results through `head -N`." (let ((lines (split-string output "\n"))) (when (> (length lines) parsed-head-limit) (setq output (string-join (seq-take lines parsed-head-limit) "\n"))))) + (when (> (length output) macher--max-read-length) + (error + "Too many matches: %d bytes exceeds maximum read length of %d bytes" + (length output) + macher--max-read-length)) output)))) (defun macher--tool-search From 068316f0141d61aac82ab07cf05613c508f496f1 Mon Sep 17 00:00:00 2001 From: "Kevin Montag (via LLM)" Date: Tue, 5 May 2026 01:30:47 +0000 Subject: [PATCH 2/7] refactor: promote macher--max-read-length to a customizable user option Rename to `macher-max-tool-output-length' and convert from defconst to defcustom. The cap was already being applied by the read tool; making it customizable lets users tune the limit for their model's context window, and reusing it for other text-returning tools (in a follow-up) needs a name that isn't tied to the read tool. Change-Id: I3f799bd094f15df69bbcc772f722d60c4f7afde0 --- macher.el | 24 ++++++++++++++---------- tests/test-integration.el | 8 ++++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/macher.el b/macher.el index 87de205..7928c74 100644 --- a/macher.el +++ b/macher.el @@ -776,7 +776,7 @@ adding tools to this category directly; instead, customize "(offset/limit) and line numbering.\n" "\n" "Returns file contents on success. Fails if the file is not found in the workspace " - "or if the content exceeds the maximum read length." + "or if the content exceeds the maximum tool output length." macher--workspace-postfix) :confirm nil :include nil @@ -1083,6 +1083,16 @@ Set to nil to disable the limit entirely." :type '(choice (natnum :tag "Maximum number of characters") (const :tag "No limit" nil)) :group 'macher-tools) +(defcustom macher-max-tool-output-length (* 1024 1024) + "Maximum number of bytes any macher tool may return to the LLM. + +Tools that produce text output (read, search, list-directory) signal an +error if their result exceeds this length, rather than dumping a large +payload into the LLM context. Lower this if your model has a small +context window." + :type 'natnum + :group 'macher-tools) + (defcustom macher-presets-alist `( ;; Enable all macher tools. @@ -1190,11 +1200,6 @@ To add a new workspace type, add an entry to this alist and update :type '(alist :key-type symbol :value-type (plist :key-type keyword :value-type function)) :group 'macher-workspace) -;;; Constants - -(defconst macher--max-read-length (* 1024 1024) - "Max number of bytes to return from the read tool.") - ;;; Variables (defvar-local macher--workspace nil @@ -2179,12 +2184,11 @@ found in the workspace." (round limit))) (processed-content (macher--read-string new-content parsed-offset parsed-limit show-line-numbers))) - ;; Check if the processed content exceeds the maximum read length. - (when (> (length processed-content) macher--max-read-length) + (when (> (length processed-content) macher-max-tool-output-length) (error - "File content too large: %d bytes exceeds maximum read length of %d bytes" + "File content too large: %d bytes exceeds maximum tool output length of %d bytes" (length processed-content) - macher--max-read-length)) + macher-max-tool-output-length)) processed-content)) nil)))) diff --git a/tests/test-integration.el b/tests/test-integration.el index f303b26..7f5073c 100644 --- a/tests/test-integration.el +++ b/tests/test-integration.el @@ -720,9 +720,9 @@ SILENT and INHIBIT-COOKIES are ignored in this mock implementation." ;; Second request should contain numbered lines (cat -n style). (expect (cadr tool-messages) :to-equal '("1\tline1\n2\tline2\n3\tline3\n4\tline4")))))) - (it "returns error when file content exceeds max read length" - ;; Create a file with content that exceeds macher--max-read-length - (let* ((large-content (make-string (1+ macher--max-read-length) ?x)) + (it "returns error when file content exceeds max tool output length" + ;; Create a file with content that exceeds macher-max-tool-output-length + (let* ((large-content (make-string (1+ macher-max-tool-output-length) ?x)) (large-file-path (expand-file-name "large-file.txt" project-dir))) (unwind-protect (progn @@ -762,7 +762,7 @@ SILENT and INHIBIT-COOKIES are ignored in this mock implementation." (let ((error-message (cadr tool-messages))) (expect (length error-message) :to-be 1) (expect "File content too large" :to-appear-once-in (car error-message)) - (expect "exceeds maximum read length" + (expect "exceeds maximum tool output length" :to-appear-once-in (car error-message))))))) ;; Clean up the large file (when (file-exists-p large-file-path) From 803c6f76d40632f75e2210138f174d9699d6db1c Mon Sep 17 00:00:00 2001 From: "Kevin Montag (via LLM)" Date: Tue, 5 May 2026 20:35:20 +0000 Subject: [PATCH 3/7] feat: cap output of search and list-directory tools Apply `macher-max-tool-output-length' to the search and list-directory tools, which previously could dump unbounded amounts of text into the LLM context. Factor the check into `macher--check-output-length' and reuse it in the read tool for consistency. When the limit is exceeded, include the leading and trailing 256 bytes of the output in the error so the caller can see the structure of the truncated payload. Change-Id: I0eca3de190b510e00957eeeb47034ec6c1e91d79 --- macher.el | 50 ++++++++++++++++++++++++++++++---------------- tests/test-unit.el | 14 +++++++++++-- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/macher.el b/macher.el index 7928c74..a2ed133 100644 --- a/macher.el +++ b/macher.el @@ -1086,10 +1086,10 @@ Set to nil to disable the limit entirely." (defcustom macher-max-tool-output-length (* 1024 1024) "Maximum number of bytes any macher tool may return to the LLM. -Tools that produce text output (read, search, list-directory) signal an -error if their result exceeds this length, rather than dumping a large -payload into the LLM context. Lower this if your model has a small -context window." +Tools that produce text output (e.g. read, search, directory listing) +signal an error if their result exceeds this length, rather than +dumping a large payload into the LLM context. Lower this if your +model has a small context window." :type 'natnum :group 'macher-tools) @@ -2139,6 +2139,27 @@ file. Also updates the context's :contents alist." ;; The workspace tools are somewhat inspired by the reference filesystem MCP. See ;; https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/README.md. +(defun macher--check-output-length (output description) + "Signal an error if OUTPUT exceeds `macher-max-tool-output-length'. +DESCRIPTION names what is being returned (e.g. \"File content\") and is +included verbatim in the error message. When the limit is exceeded, +the error includes the leading and trailing portions of OUTPUT so the +caller can see the structure of the truncated payload." + (let ((len (length output))) + (when (> len macher-max-tool-output-length) + (let* ((preview-bytes (min 256 (/ len 2))) + (head (substring output 0 preview-bytes)) + (tail (substring output (- len preview-bytes)))) + (error + "%s too large: %d bytes exceeds maximum tool output length of %d bytes\nFirst %d bytes:\n%s\n...\nLast %d bytes:\n%s" + description + len + macher-max-tool-output-length + preview-bytes + head + preview-bytes + tail))))) + (defun macher--tool-read-file (context path &optional offset limit show-line-numbers) "Read the contents of a file specified by PATH within the workspace. @@ -2184,11 +2205,7 @@ found in the workspace." (round limit))) (processed-content (macher--read-string new-content parsed-offset parsed-limit show-line-numbers))) - (when (> (length processed-content) macher-max-tool-output-length) - (error - "File content too large: %d bytes exceeds maximum tool output length of %d bytes" - (length processed-content) - macher-max-tool-output-length)) + (macher--check-output-length processed-content "File content") processed-content)) nil)))) @@ -2425,9 +2442,12 @@ Signals an error if the directory is not found in the workspace." (collect-entries full-path "" 0)) ;; Return formatted results. - (if results - (string-join (reverse results) "\n") - "Directory is empty"))) + (let ((output + (if results + (string-join (reverse results) "\n") + "Directory is empty"))) + (macher--check-output-length output "Directory listing") + output))) (defun macher--tool-edit-file (context path old-text new-text &optional replace-all) "Edit file specified by PATH within the workspace. @@ -3081,11 +3101,7 @@ piping the results through `head -N`." (let ((lines (split-string output "\n"))) (when (> (length lines) parsed-head-limit) (setq output (string-join (seq-take lines parsed-head-limit) "\n"))))) - (when (> (length output) macher--max-read-length) - (error - "Too many matches: %d bytes exceeds maximum read length of %d bytes" - (length output) - macher--max-read-length)) + (macher--check-output-length output "Search output") output)))) (defun macher--tool-search diff --git a/tests/test-unit.el b/tests/test-unit.el index bb28029..5f511df 100644 --- a/tests/test-unit.el +++ b/tests/test-unit.el @@ -1338,7 +1338,12 @@ ;; Clean up the external directory (when (file-exists-p external-dir) - (delete-directory external-dir t)))))) + (delete-directory external-dir t))))) + + (it "errors when output exceeds the configured max" + ;; Lower the cap so we don't need a huge directory to exercise the path. + (let ((macher-max-tool-output-length 5)) + (expect (macher--tool-list-directory context ".") :to-throw 'error)))) (describe "macher--search-get-xref-matches" :var (context temp-dir) @@ -2785,7 +2790,12 @@ ;; 0.4 should round to 0 (no extra lines). (expect result-0.4 :to-match "hello") ;; 0.6 should round to 1 (1 extra line). - (expect result-0.6 :to-match "hello"))))) + (expect result-0.6 :to-match "hello")))) + + (it "errors when output exceeds the configured max" + ;; Lower the cap to force the error without needing a huge fixture. + (let ((macher-max-tool-output-length 5)) + (expect (macher--tool-search-helper context "hello") :to-throw 'error)))) (describe "macher--tool-read-file" :var (context temp-dir) From f2c0880e2ecb4bced356889544243afccaeea172 Mon Sep 17 00:00:00 2001 From: Kevin Montag Date: Wed, 6 May 2026 02:41:24 +0200 Subject: [PATCH 4/7] test: cover macher--check-output-length Exercise the under-limit/at-limit no-op path, the over-limit error, the contents of the error message (description, sizes, head/tail previews), and the small-payload preview-shrinking branch. Change-Id: I6bab46f6d4d307f54592dac246525d8d4ce7109b --- tests/test-unit.el | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test-unit.el b/tests/test-unit.el index 5f511df..30a94b7 100644 --- a/tests/test-unit.el +++ b/tests/test-unit.el @@ -880,6 +880,54 @@ ;; Should match the literal string "array[0]", not as regex. (expect result :to-equal "list[0] = value"))))) + (describe "macher--check-output-length" + (it "returns nil when output is within the limit" + (let ((macher-max-tool-output-length 100)) + (expect (macher--check-output-length "short" "Thing") :to-be nil) + ;; Boundary: exactly at the limit is allowed. + (expect (macher--check-output-length (make-string 100 ?x) "Thing") :to-be nil))) + + (it "signals an error when output exceeds the limit" + (let ((macher-max-tool-output-length 10) + (output (make-string 11 ?x))) + (expect (macher--check-output-length output "Thing") :to-throw 'error))) + + (it "includes description, sizes, and head/tail previews in the error" + ;; Use a large-enough payload that the preview is capped at 256 bytes. + (let* ((macher-max-tool-output-length 10) + (head (make-string 256 ?A)) + (mid (make-string 600 ?M)) + (tail (make-string 256 ?Z)) + (output (concat head mid tail)) + (err + (condition-case e + (macher--check-output-length output "Search output") + (error + e))) + (msg (error-message-string err))) + (expect msg :to-match "\\`Search output too large: 1112 bytes") + (expect msg :to-match "maximum tool output length of 10 bytes") + (expect msg :to-match "First 256 bytes:\n") + (expect msg :to-match "Last 256 bytes:\n") + ;; Previews reflect the actual head/tail of the payload. + (expect msg :to-match (regexp-quote head)) + (expect msg :to-match (regexp-quote tail)) + ;; The middle of the payload is omitted. + (expect msg :not :to-match (regexp-quote mid)))) + + (it "shrinks the preview to half the payload for small over-length outputs" + ;; A 5-byte payload over a 1-byte cap previews 2 bytes (= 5/2) on each end. + (let* ((macher-max-tool-output-length 1) + (output "abcde") + (err + (condition-case e + (macher--check-output-length output "Thing") + (error + e))) + (msg (error-message-string err))) + (expect msg :to-match "First 2 bytes:\nab") + (expect msg :to-match "Last 2 bytes:\nde")))) + (describe "macher--format-size" (it "formats bytes correctly" (expect (macher--format-size 0) :to-equal "0 B") From 0fa37faa7ceb8d36d908973761851ec069c5f14a Mon Sep 17 00:00:00 2001 From: Kevin Montag Date: Wed, 6 May 2026 02:41:24 +0200 Subject: [PATCH 5/7] test: cover macher--check-output-length Exercise the under-limit/at-limit no-op path, the over-limit error, the contents of the error message (description, sizes, head/tail previews), and the small-payload preview-shrinking branch. Change-Id: I6bab46f6d4d307f54592dac246525d8d4ce7109b --- tests/test-unit.el | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test-unit.el b/tests/test-unit.el index 5f511df..30a94b7 100644 --- a/tests/test-unit.el +++ b/tests/test-unit.el @@ -880,6 +880,54 @@ ;; Should match the literal string "array[0]", not as regex. (expect result :to-equal "list[0] = value"))))) + (describe "macher--check-output-length" + (it "returns nil when output is within the limit" + (let ((macher-max-tool-output-length 100)) + (expect (macher--check-output-length "short" "Thing") :to-be nil) + ;; Boundary: exactly at the limit is allowed. + (expect (macher--check-output-length (make-string 100 ?x) "Thing") :to-be nil))) + + (it "signals an error when output exceeds the limit" + (let ((macher-max-tool-output-length 10) + (output (make-string 11 ?x))) + (expect (macher--check-output-length output "Thing") :to-throw 'error))) + + (it "includes description, sizes, and head/tail previews in the error" + ;; Use a large-enough payload that the preview is capped at 256 bytes. + (let* ((macher-max-tool-output-length 10) + (head (make-string 256 ?A)) + (mid (make-string 600 ?M)) + (tail (make-string 256 ?Z)) + (output (concat head mid tail)) + (err + (condition-case e + (macher--check-output-length output "Search output") + (error + e))) + (msg (error-message-string err))) + (expect msg :to-match "\\`Search output too large: 1112 bytes") + (expect msg :to-match "maximum tool output length of 10 bytes") + (expect msg :to-match "First 256 bytes:\n") + (expect msg :to-match "Last 256 bytes:\n") + ;; Previews reflect the actual head/tail of the payload. + (expect msg :to-match (regexp-quote head)) + (expect msg :to-match (regexp-quote tail)) + ;; The middle of the payload is omitted. + (expect msg :not :to-match (regexp-quote mid)))) + + (it "shrinks the preview to half the payload for small over-length outputs" + ;; A 5-byte payload over a 1-byte cap previews 2 bytes (= 5/2) on each end. + (let* ((macher-max-tool-output-length 1) + (output "abcde") + (err + (condition-case e + (macher--check-output-length output "Thing") + (error + e))) + (msg (error-message-string err))) + (expect msg :to-match "First 2 bytes:\nab") + (expect msg :to-match "Last 2 bytes:\nde")))) + (describe "macher--format-size" (it "formats bytes correctly" (expect (macher--format-size 0) :to-equal "0 B") From 2ca5aad483fa2ef0405cd405985e2a26624864ba Mon Sep 17 00:00:00 2001 From: Kevin Montag Date: Wed, 6 May 2026 04:29:23 +0200 Subject: [PATCH 6/7] Change new custom variable to `macher-tool-output-max-length`, add to README --- README.md | 15 +++++++++------ macher.el | 22 +++++++++++----------- tests/test-integration.el | 4 ++-- tests/test-unit.el | 24 ++++++++++++------------ 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index f055fc5..7535168 100644 --- a/README.md +++ b/README.md @@ -277,12 +277,15 @@ You can see customizable variables/sub-groups with `M-x customize-group RET mach ### Tools -| Variable | Description | -| -------------------------- | -------------------------------------------- | -| `macher-tools` | Tool definitions for reading/editing files | -| `macher-presets-alist` | Preset definitions (macher, macher-ro, etc.) | -| `macher-tool-category` | Category for macher tools in gptel registry | -| `macher-match-max-columns` | Max line length for search results | +| Variable | Description | +| ------------------------------- | ------------------------------------------------------------------ | +| Variable | Description | +| ------------------------------- | ------------------------------------------------------------ | +| `macher-tools` | Tool definitions for reading/editing files | +| `macher-presets-alist` | Preset definitions (macher, macher-ro, etc.) | +| `macher-tool-category` | Category for macher tools in gptel registry | +| `macher-match-max-columns` | Max line length for search results | +| `macher-tool-output-max-length` | Max characters any macher tool may return (errors out if exceeded) | ## FAQ diff --git a/macher.el b/macher.el index 6eea542..aa58a56 100644 --- a/macher.el +++ b/macher.el @@ -1091,8 +1091,8 @@ Set to nil to disable the limit entirely." :type '(choice (natnum :tag "Maximum number of characters") (const :tag "No limit" nil)) :group 'macher-tools) -(defcustom macher-max-tool-output-length (* 1024 1024) - "Maximum number of bytes any macher tool may return to the LLM. +(defcustom macher-tool-output-max-length (* 1024 1024) + "Maximum number of characters any macher tool may return to the LLM. Tools that produce text output (e.g. read, search, directory listing) signal an error if their result exceeds this length, rather than @@ -2211,24 +2211,24 @@ a redundant computation over a remote connection." ;; https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/README.md. (defun macher--check-output-length (output description) - "Signal an error if OUTPUT exceeds `macher-max-tool-output-length'. + "Signal an error if OUTPUT exceeds `macher-tool-output-max-length'. DESCRIPTION names what is being returned (e.g. \"File content\") and is included verbatim in the error message. When the limit is exceeded, the error includes the leading and trailing portions of OUTPUT so the caller can see the structure of the truncated payload." (let ((len (length output))) - (when (> len macher-max-tool-output-length) - (let* ((preview-bytes (min 256 (/ len 2))) - (head (substring output 0 preview-bytes)) - (tail (substring output (- len preview-bytes)))) + (when (> len macher-tool-output-max-length) + (let* ((preview-chars (min 256 (/ len 2))) + (head (substring output 0 preview-chars)) + (tail (substring output (- len preview-chars)))) (error - "%s too large: %d bytes exceeds maximum tool output length of %d bytes\nFirst %d bytes:\n%s\n...\nLast %d bytes:\n%s" + "%s too large: %d characters exceeds maximum tool output length of %d characters\nFirst %d characters:\n%s\n...\nLast %d characters:\n%s" description len - macher-max-tool-output-length - preview-bytes + macher-tool-output-max-length + preview-chars head - preview-bytes + preview-chars tail))))) (defun macher--tool-read-file (context path &optional offset limit show-line-numbers) diff --git a/tests/test-integration.el b/tests/test-integration.el index 7f5073c..6d9b5fe 100644 --- a/tests/test-integration.el +++ b/tests/test-integration.el @@ -721,8 +721,8 @@ SILENT and INHIBIT-COOKIES are ignored in this mock implementation." (expect (cadr tool-messages) :to-equal '("1\tline1\n2\tline2\n3\tline3\n4\tline4")))))) (it "returns error when file content exceeds max tool output length" - ;; Create a file with content that exceeds macher-max-tool-output-length - (let* ((large-content (make-string (1+ macher-max-tool-output-length) ?x)) + ;; Create a file with content that exceeds macher-tool-output-max-length + (let* ((large-content (make-string (1+ macher-tool-output-max-length) ?x)) (large-file-path (expand-file-name "large-file.txt" project-dir))) (unwind-protect (progn diff --git a/tests/test-unit.el b/tests/test-unit.el index 76cf95f..11f117c 100644 --- a/tests/test-unit.el +++ b/tests/test-unit.el @@ -905,19 +905,19 @@ (describe "macher--check-output-length" (it "returns nil when output is within the limit" - (let ((macher-max-tool-output-length 100)) + (let ((macher-tool-output-max-length 100)) (expect (macher--check-output-length "short" "Thing") :to-be nil) ;; Boundary: exactly at the limit is allowed. (expect (macher--check-output-length (make-string 100 ?x) "Thing") :to-be nil))) (it "signals an error when output exceeds the limit" - (let ((macher-max-tool-output-length 10) + (let ((macher-tool-output-max-length 10) (output (make-string 11 ?x))) (expect (macher--check-output-length output "Thing") :to-throw 'error))) (it "includes description, sizes, and head/tail previews in the error" ;; Use a large-enough payload that the preview is capped at 256 bytes. - (let* ((macher-max-tool-output-length 10) + (let* ((macher-tool-output-max-length 10) (head (make-string 256 ?A)) (mid (make-string 600 ?M)) (tail (make-string 256 ?Z)) @@ -928,10 +928,10 @@ (error e))) (msg (error-message-string err))) - (expect msg :to-match "\\`Search output too large: 1112 bytes") - (expect msg :to-match "maximum tool output length of 10 bytes") - (expect msg :to-match "First 256 bytes:\n") - (expect msg :to-match "Last 256 bytes:\n") + (expect msg :to-match "\\`Search output too large: 1112 characters") + (expect msg :to-match "maximum tool output length of 10 characters") + (expect msg :to-match "First 256 characters:\n") + (expect msg :to-match "Last 256 characters:\n") ;; Previews reflect the actual head/tail of the payload. (expect msg :to-match (regexp-quote head)) (expect msg :to-match (regexp-quote tail)) @@ -940,7 +940,7 @@ (it "shrinks the preview to half the payload for small over-length outputs" ;; A 5-byte payload over a 1-byte cap previews 2 bytes (= 5/2) on each end. - (let* ((macher-max-tool-output-length 1) + (let* ((macher-tool-output-max-length 1) (output "abcde") (err (condition-case e @@ -948,8 +948,8 @@ (error e))) (msg (error-message-string err))) - (expect msg :to-match "First 2 bytes:\nab") - (expect msg :to-match "Last 2 bytes:\nde")))) + (expect msg :to-match "First 2 characters:\nab") + (expect msg :to-match "Last 2 characters:\nde")))) (describe "macher--format-size" (it "formats bytes correctly" @@ -1413,7 +1413,7 @@ (it "errors when output exceeds the configured max" ;; Lower the cap so we don't need a huge directory to exercise the path. - (let ((macher-max-tool-output-length 5)) + (let ((macher-tool-output-max-length 5)) (expect (macher--tool-list-directory context ".") :to-throw 'error)))) (describe "macher--search-get-xref-matches" @@ -2879,7 +2879,7 @@ (it "errors when output exceeds the configured max" ;; Lower the cap to force the error without needing a huge fixture. - (let ((macher-max-tool-output-length 5)) + (let ((macher-tool-output-max-length 5)) (expect (macher--tool-search-helper context "hello") :to-throw 'error))) (it "search handles nonexistent files in workspace gracefully" From 8b91af371981a4c0996b8ba1d0ee399dba5f7e34 Mon Sep 17 00:00:00 2001 From: Kevin Montag Date: Wed, 6 May 2026 04:32:52 +0200 Subject: [PATCH 7/7] Fix README typo --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 7535168..8cc9e97 100644 --- a/README.md +++ b/README.md @@ -279,8 +279,6 @@ You can see customizable variables/sub-groups with `M-x customize-group RET mach | Variable | Description | | ------------------------------- | ------------------------------------------------------------------ | -| Variable | Description | -| ------------------------------- | ------------------------------------------------------------ | | `macher-tools` | Tool definitions for reading/editing files | | `macher-presets-alist` | Preset definitions (macher, macher-ro, etc.) | | `macher-tool-category` | Category for macher tools in gptel registry |