-
Notifications
You must be signed in to change notification settings - Fork 336
Tools collection
karthink edited this page Dec 7, 2025
·
20 revisions
This page catalogs some user-contributed tools for use with gptel's tool-use feature. These tools allow LLMs to perform various actions within Emacs, access external services, and manipulate data.
These tools are meant to serve as examples for writing your own tools. For continuously maintained and well-tested tools that plug into gptel, please use llm-tool-collection or gptel-agent.
To use any of these tools, you need to:
- Enable tool-use in gptel:
(setq gptel-use-tools t) - Add the tool definitions you want to use to
gptel-tools - Copy and evaluate the Elisp code for the tools you want to use
Example:
;; Enable tool use
(setq gptel-use-tools t)
;; Add a tool to gptel-tools
(add-to-list 'gptel-tools
(gptel-make-tool
:name "read_url"
:function (lambda (url)
;; function implementation
)
:description "Fetch and read the contents of a URL"
:args (list '(:name "url"
:type string
:description "The URL to read"))
:category "web"))For more tools and discussions:
- llm-tool-collection - A community collection of tools for use with LLM clients in Emacs. To discuss tools, check out the discussions page
- ragmacs - A Collection of tools for Emacs introspection.
- codel.el - A collection of code editing tools.
- gptel-org-tools - Tools for interacting with Org files.
- Alain Lafon’s tool collection - Additional tools not listed here.
Tools provided by MCP servers are provided via the mcp.el package.
Below are examples of some gptel tools.
(gptel-make-tool
:function (lambda (filepath)
(with-temp-buffer
(insert-file-contents (expand-file-name filepath))
(buffer-string)))
:name "read_file"
:description "Read and display the contents of a file"
:args (list '(:name "filepath"
:type string
:description "Path to the file to read. Supports relative paths and ~."))
:category "filesystem")(gptel-make-tool
:function (lambda (directory)
(mapconcat #'identity
(directory-files directory)
"\n"))
:name "list_directory"
:description "List the contents of a given directory"
:args (list '(:name "directory"
:type string
:description "The path to the directory to list"))
:category "filesystem")(gptel-make-tool
:function (lambda (parent name)
(condition-case nil
(progn
(make-directory (expand-file-name name parent) t)
(format "Directory %s created/verified in %s" name parent))
(error (format "Error creating directory %s in %s" name parent))))
:name "make_directory"
:description "Create a new directory with the given name in the specified parent directory"
:args (list '(:name "parent"
:type string
:description "The parent directory where the new directory should be created, e.g. /tmp")
'(:name "name"
:type string
:description "The name of the new directory to create, e.g. testdir"))
:category "filesystem")(gptel-make-tool
:function (lambda (path filename content)
(let ((full-path (expand-file-name filename path)))
(with-temp-buffer
(insert content)
(write-file full-path))
(format "Created file %s in %s" filename path)))
:name "create_file"
:description "Create a new file with the specified content"
:args (list '(:name "path"
:type string
:description "The directory where to create the file")
'(:name "filename"
:type string
:description "The name of the file to create")
'(:name "content"
:type string
:description "The content to write to the file"))
:category "filesystem")(defun my-gptel--edit_file (file-path file-edits)
"In FILE-PATH, apply FILE-EDITS with pattern matching and replacing."
(if (and file-path (not (string= file-path "")) file-edits)
(with-current-buffer (get-buffer-create "*edit-file*")
(erase-buffer)
(insert-file-contents (expand-file-name file-path))
(let ((inhibit-read-only t)
(case-fold-search nil)
(file-name (expand-file-name file-path))
(edit-success nil))
;; apply changes
(dolist (file-edit (seq-into file-edits 'list))
(when-let ((line-number (plist-get file-edit :line_number))
(old-string (plist-get file-edit :old_string))
(new-string (plist-get file-edit :new_string))
(is-valid-old-string (not (string= old-string ""))))
(goto-char (point-min))
(forward-line (1- line-number))
(when (search-forward old-string nil t)
(replace-match new-string t t)
(setq edit-success t))))
;; return result to gptel
(if edit-success
(progn
;; show diffs
(ediff-buffers (find-file-noselect file-name) (current-buffer))
(format "Successfully edited %s" file-name))
(format "Failed to edited %s" file-name))))
(format "Failed to edited %s" file-path)))
(gptel-make-tool
:function #'my-gptel--edit_file
:name "edit_file"
:description "Edit file with a list of edits, each edit contains a line-number,
a old-string and a new-string, new-string will replace the old-string at the specified line."
:args (list '(:name "file-path"
:type string
:description "The full path of the file to edit")
'(:name "file-edits"
:type array
:items (:type object
:properties
(:line_number
(:type integer :description "The line number of the file where edit starts.")
:old_string
(:type string :description "The old-string to be replaced.")
:new_string
(:type string :description "The new-string to replace old-string.")))
:description "The list of edits to apply on the file"))
:category "filesystem") (gptel-make-tool
:function (lambda (command &optional working_dir)
(with-temp-message (format "Executing command: `%s`" command)
(let ((default-directory (if (and working_dir (not (string= working_dir "")))
(expand-file-name working_dir)
default-directory)))
(shell-command-to-string command))))
:name "run_command"
:description "Executes a shell command and returns the output as a string. IMPORTANT: This tool allows execution of arbitrary code; user confirmation will be required before any command is run."
:args (list
'(:name "command"
:type string
:description "The complete shell command to execute.")
'(:name "working_dir"
:type string
:description "Optional: The directory in which to run the command. Defaults to the current directory if not specified."))
:category "command"
:confirm t
:include t)
(defun run_async_command (callback command)
"Run COMMAND asynchronously and pass output to CALLBACK."
(condition-case error
(let ((buffer (generate-new-buffer " *async output*")))
(with-temp-message (format "Running async command: %s" command)
(async-shell-command command buffer nil))
(let ((proc (get-buffer-process buffer)))
(when proc
(set-process-sentinel
proc
(lambda (process _event)
(unless (process-live-p process)
(with-current-buffer (process-buffer process)
(let ((output (buffer-substring-no-properties (point-min) (point-max))))
(kill-buffer (current-buffer))
(funcall callback output)))))))))
(t
;; Handle any kind of error
(funcall callback (format "An error occurred: %s" error)))))
(gptel-make-tool
:function #'run_async_command
:name "run_async_command"
:description "Run an async command."
:args (list
'(:name "command"
:type "string"
:description "Command to run."))
:category "command"
:async t
:include t)(gptel-make-tool
:function (lambda (text)
(message "%s" text)
(format "Message sent: %s" text))
:name "echo_message"
:description "Send a message to the *Messages* buffer"
:args (list '(:name "text"
:type string
:description "The text to send to the messages buffer"))
:category "emacs")(defun gptel-read-documentation (symbol)
"Read the documentation for SYMBOL, which can be a function or variable."
(let ((sym (intern symbol)))
(cond
((fboundp sym)
(documentation sym))
((boundp sym)
(documentation-property sym 'variable-documentation))
(t
(format "No documentation found for %s" symbol)))))
(gptel-make-tool
:name "read_documentation"
:function #'gptel-read-documentation
:description "Read the documentation for a given function or variable"
:args (list '(:name "name"
:type string
:description "The name of the function or variable whose documentation is to be retrieved"))
:category "emacs")(gptel-make-tool
:function (lambda (url)
(with-current-buffer (url-retrieve-synchronously url)
(goto-char (point-min))
(forward-paragraph)
(let ((dom (libxml-parse-html-region (point) (point-max))))
(run-at-time 0 nil #'kill-buffer (current-buffer))
(with-temp-buffer
(shr-insert-document dom)
(buffer-substring-no-properties (point-min) (point-max))))))
:name "read_url"
:description "Fetch and read the contents of a URL"
:args (list '(:name "url"
:type string
:description "The URL to read"))
:category "web")(defvar brave-search-api-key "<YOUR API KEY>"
"API key for accessing the Brave Search API.")
(defun brave-search-query (query)
"Perform a web search using the Brave Search API with the given QUERY."
(let ((url-request-method "GET")
(url-request-extra-headers `(("X-Subscription-Token" . ,brave-search-api-key)))
(url (format "https://api.search.brave.com/res/v1/web/search?q=%s" (url-encode-url query))))
(with-current-buffer (url-retrieve-synchronously url)
(goto-char (point-min))
(when (re-search-forward "^$" nil 'move)
(let ((json-object-type 'hash-table)) ; Use hash-table for JSON parsing
(json-parse-string (buffer-substring-no-properties (point) (point-max))))))))
(gptel-make-tool
:function #'brave-search-query
:name "brave_search"
:description "Perform a web search using the Brave Search API"
:args (list '(:name "query"
:type string
:description "The search query string"))
:category "web")(defun my/gptel-youtube-metadata (callback url)
(let* ((video-id (and (string-match (concat
"^\\(?:http\\(?:s?://\\)\\)?\\(?:www\\.\\)?\\(?:youtu\\(?:\\(?:\\.be\\|be\\.com\\)/\\)\\)"
"\\(?:watch\\?v=\\)?"
"\\([^?&]+\\)")
url)
(match-string 1 url)))
(dir (file-name-concat temporary-file-directory "yt-dlp" video-id)))
(if (file-directory-p dir)
(delete-directory dir t))
(make-directory dir t)
(let ((default-directory dir)
(idx 0)
(data (list :description nil :transcript nil)))
(make-process :name "yt-dlp"
:command `("yt-dlp" "--write-description" "--skip-download" "--output" "video" ,url)
:sentinel (lambda (proc status)
(cl-incf idx)
(let ((default-directory dir))
(when (file-readable-p "video.description")
(plist-put data :description
(with-temp-buffer
(insert-file-contents "video.description")
(buffer-string)))))
(when (= idx 2)
(funcall callback (gptel--json-encode data))
(delete-directory dir t))))
(make-process :name "yt-dlp"
:command `("yt-dlp" "--skip-download" "--write-auto-subs" "--sub-langs" "en,-live_chat" "--convert-subs" "srt" "--output" "video" ,url)
:sentinel (lambda (proc status)
(cl-incf idx)
(let ((default-directory dir))
(when (file-readable-p "video.en.srt")
(plist-put data :transcript
(with-temp-buffer
(insert-file-contents "video.en.srt")
(buffer-string)))))
(when (= idx 2)
(funcall callback (gptel--json-encode data))
(delete-directory dir t)))))))
(gptel-make-tool
:name "youtube_video_metadata"
:function #'my/gptel-youtube-metadata
:description "Find the description and video transcript for a youtube video. Return a JSON object containing two fields:
\"description\": The video description added by the uploader
\"transcript\": The video transcript in SRT format"
:args '((:name "url"
:description "The youtube video URL, for example \"https://www.youtube.com/watch?v=H2qJRnV8ZGA\""
:type string))
:category "web"
:async t
:include t)(defun my/ejc-sql-eval-query (query &optional analyze-p connection-name)
"Evaluate a SQL query using ejc-sql, ensuring a connection exists.
It takes a SQL query and the name of an existing ejc connection.
If no connection name is provided, it defaults to \"Default\".
If ANALYZE-P is non-nil, performs EXPLAIN ANALYZE on the query.
It will create a temporary buffer, connect to the database specified
by CONNECTION-NAME, evaluate the query, and return the result as a string.
It expects the connection CONNECTION-NAME to exist
using `ejc-connect'."
(interactive)
(let ((buffer (generate-new-buffer " *temp-ejc-sql-buffer*"))
(result "")
(actual-connection-name (or connection-name "Default"))
(max-wait-time 30) ; Maximum wait time in seconds
(wait-interval 0.1))
(with-current-buffer buffer
(ejc-connect actual-connection-name)
(if analyze-p
(insert (concat "EXPLAIN (ANALYZE true, COSTS true, FORMAT json) " query ";"))
(insert (concat "SELECT json_agg(row_to_json (t)) FROM (" query ") t;")))
(ejc-eval-user-sql-at-point))
(let ((wait-time 0))
(while (and (not (get-buffer "*ejc-sql-output*"))
(< wait-time max-wait-time))
(sit-for wait-interval)
(setq wait-time (+ wait-time wait-interval))))
(when (get-buffer "*ejc-sql-output*")
(with-current-buffer "*ejc-sql-output*"
(setq result (buffer-string)))
(kill-buffer "*ejc-sql-output*"))
(kill-buffer buffer)
result))
(gptel-make-tool
:name "select_from_db"
:function (lambda (query &optional analyze-p connection-name)
(my/ejc-sql-eval-query query analyze-p connection-name))
:description "Evaluate a SQL query using ejc-sql, ensuring a connection exists.
It takes a SQL query and the name of an existing ejc connection.
If no connection name is provided, it defaults to \"Default\".
If ANALYZE-P is non-nil, performs EXPLAIN ANALYZE on the query.
It will create a temporary buffer, connect to the database specified
by CONNECTION-NAME, evaluate the query, and return the result as a string."
:args (list '(:name "query"
:type string
:description "The SQL query to evaluate")
'(:name "analyze_p"
:type string
:optional t
:description "If ANALYZE-P is non-nil, performs EXPLAIN ANALYZE on the query instead of executing it directly, returning query execution plan and statistics.")
'(:name "connection_name"
:type string
:optional t
:description "The name of the ejc-sql connection to use. This connection must already exist. Defaults to 'Default' if not provided."))
:category "sql")(gptel-make-tool
:function (lambda (buffer)
(unless (buffer-live-p (get-buffer buffer))
(error "Error: buffer %s is not live." buffer))
(with-current-buffer buffer
(buffer-substring-no-properties (point-min) (point-max))))
:name "read_buffer"
:description "Return the contents of an Emacs buffer"
:args (list '(:name "buffer"
:type string
:description "The name of the buffer whose contents are to be retrieved"))
:category "emacs")(gptel-make-tool
:function (lambda (buffer text)
(with-current-buffer (get-buffer-create buffer)
(save-excursion
(goto-char (point-max))
(insert text)))
(format "Appended text to buffer %s" buffer))
:name "append_to_buffer"
:description "Append text to an Emacs buffer. If the buffer does not exist, it will be created."
:args (list '(:name "buffer"
:type string
:description "The name of the buffer to append text to.")
'(:name "text"
:type string
:description "The text to append to the buffer."))
:category "emacs")(defun codel-edit-buffer (buffer-name old-string new-string)
"In BUFFER-NAME, replace OLD-STRING with NEW-STRING."
(with-current-buffer buffer-name
(let ((case-fold-search nil)) ;; Case-sensitive search
(save-excursion
(goto-char (point-min))
(let ((count 0))
(while (search-forward old-string nil t)
(setq count (1+ count)))
(if (= count 0)
(format "Error: Could not find text to replace in buffer %s" buffer-name)
(if (> count 1)
(format "Error: Found %d matches for the text to replace in buffer %s" count buffer-name)
(goto-char (point-min))
(search-forward old-string)
(replace-match new-string t t)
(format "Successfully edited buffer %s" buffer-name))))))))
(gptel-make-tool
:name "EditBuffer"
:function #'codel-edit-buffer
:description "Edits Emacs buffers"
:args '((:name "buffer_name"
:type string
:description "Name of the buffer to modify"
:required t)
(:name "old_string"
:type string
:description "Text to replace (must match exactly)"
:required t)
(:name "new_string"
:type string
:description "Text to replace old_string with"
:required t))
:category "edit")(defun codel-replace-buffer (buffer-name content)
"Completely replace contents of BUFFER-NAME with CONTENT."
(with-current-buffer buffer-name
(erase-buffer)
(insert content)
(format "Buffer replaced: %s" buffer-name)))
(gptel-make-tool
:name "ReplaceBuffer"
:function #'codel-replace-buffer
:description "Completely overwrites buffer contents"
:args '((:name "buffer_name"
:type string
:description "Name of the buffer to overwrite"
:required t)
(:name "content"
:type string
:description "Content to write to the buffer"
:required t))
:category "edit")