branch: elpa/gptel
commit 18a5eb413cc09f8c449462e4a39656954437ccfc
Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>

    gptel-integrations: Add utilities for MCP
    
    * gptel-integrations.el: Add a new file with utilities for
    integrating with mcp.el, the Emacs client for MCP.  This file
    contains helper commands for integrating with the mcp.el package:
    
    (gptel-mcp-connect): Register gptel-tools from MCP servers.  This
    command guides the user through the mcp.el setup
    process (somewhat) when called interactively.
    (gptel-mcp-disconnect): Unregister tools from MCP servers
    
    Neither of these commands is autoloaded, and to use this library
    the user must explicitly run (require 'gptel-integrations).  This
    is to avoid cluttering the experience and Emacs session of users
    who have no interest in using tools/MCP.
    
    Once this feature is loaded, these commands will also be available
    from gptel's tools menu, via `M-x gptel-tools' or `gptel-menu'.
    
    (gptel--suffix-mcp-connect, gptel--suffix-mcp-disconnect):
    Transient menu versions of the above commands.
    
    In the future support for the llm-tools-collection package is
    planned.
    
    * README: Document gptel-integrations.
---
 README.org            |  31 +++++++++-
 gptel-integrations.el | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 184 insertions(+), 2 deletions(-)

diff --git a/README.org b/README.org
index 1bebbbf406..cc91244500 100644
--- a/README.org
+++ b/README.org
@@ -111,7 +111,7 @@ gptel uses Curl if available, but falls back to the 
built-in url-retrieve to wor
       - [[#openrouter][OpenRouter]]
       - [[#privategpt][PrivateGPT]]
       - [[#deepseek][DeepSeek]]
-      - [[#sambanova-deepseek][Sambanova (DeepSeek)]]
+      - [[#sambanova-deepseek][Sambanova (Deepseek)]]
       - [[#cerebras][Cerebras]]
       - [[#github-models][Github Models]]
       - [[#novita-ai][Novita AI]]
@@ -128,6 +128,7 @@ gptel uses Curl if available, but falls back to the 
built-in url-retrieve to wor
     - [[#tool-use][Tool use]]
       - [[#defining-gptel-tools][Defining gptel tools]]
       - [[#selecting-tools][Selecting tools]]
+      - [[#tools-from-mcp-servers][Tools from MCP servers]]
     - [[#rewrite-refactor-or-fill-in-a-region][Rewrite, refactor or fill in a 
region]]
     - [[#extra-org-mode-conveniences][Extra Org mode conveniences]]
   - [[#faq][FAQ]]
@@ -1179,14 +1180,40 @@ With some prompting, you can get an LLM to write these 
tools for you.
 Tools can also be asynchronous, use optional arguments and arguments with more 
structure (enums, arrays, objects etc).  See =gptel-make-tool= for details.
 
 #+html: </details>
+#+html: <details><summary>
 **** Selecting tools
-
+#+html: </summary>
 Once defined, tools can be selected (globally, buffer-locally or for the next 
request only) from gptel's transient menu:
 
 #+html: <img 
src="https://github.com/user-attachments/assets/fd878596-b313-4385-b675-3d6546909d8b";
 align="center" alt="Image showing gptel's tool selection menu.">
 
 From here you can also require confirmation for all tool calls, and decide if 
tool call results should be included in the LLM response.  See 
[[#additional-configuration][Additional Configuration]] for doing these things 
via elisp.
 
+#+html: </details>
+#+html: <details><summary>
+**** Tools from MCP servers
+#+html: </summary>
+
+The [[https://modelcontextprotocol.io/introduction][Model Context Protocol]] 
(MCP) is a protocol for providing resources and tools to LLMs, and 
[[https://github.com/appcypher/awesome-mcp-servers][many MCP servers exist]] 
that provide LLM tools for file access, database connections, API integrations 
etc.  The [[mcp.el]] package for Emacs can act as an MCP client and manage 
these tool calls for gptel.
+
+To use MCP servers with gptel, you thus need three pieces:
+
+1. The [[https://github.com/lizqwerscott/mcp.el][mcp.el]] package for Emacs
+2. MCP servers configured for and running via mcp.el.  (MCP servers are 
typically run locally.)
+3. gptel and access to an LLM
+
+While mcp.el 
[[https://github.com/lizqwerscott/mcp.el?tab=readme-ov-file#use-with-gptel][provides
 instructions]] for accessing MCP-provided tools from gptel, gptel includes 
=gptel-integrations=, a small library to make this more convenient.  This 
library is not automatically loaded by gptel, so if you would like to use it 
you have to require it:
+
+#+begin_src emacs-lisp
+(require 'gptel-integrations)
+#+end_src
+
+Once loaded, you can run the =gptel-mcp-connect= and =gptel-mcp-disconnect= 
commands to register and unregister MCP-provided tools in gptel.  These will 
also show up in the tools menu in gptel, accessed via =M-x gptel-menu= or =M-x 
gptel-tools=:
+
+#+html: <img 
src="https://github.com/user-attachments/assets/2cbbf8a0-49c7-49a5-ba24-514ad7e08799";
 align="center" alt="Image showing MCP tool registration commands in gptel's 
tool selection menu.">
+
+MCP-provided tools can be used as normal with gptel.
+
 *** Rewrite, refactor or fill in a region
 
 In any buffer: with a region selected, you can modify text, rewrite prose or 
refactor code with =gptel-rewrite=.  Example with prose:
diff --git a/gptel-integrations.el b/gptel-integrations.el
new file mode 100644
index 0000000000..6f10eb13aa
--- /dev/null
+++ b/gptel-integrations.el
@@ -0,0 +1,155 @@
+;;; gptel-transient.el --- Integrations for gptel  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025  Karthik Chikmagalur
+
+;; Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
+;; Keywords: convenience
+
+;; SPDX-License-Identifier: GPL-3.0-or-later
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Integrations with related packages for gptel.  To use these, run
+;;
+;; (require 'gptel-integrations)
+;;
+;; For MCP integration:
+;; - Run M-x `gptel-mcp-connect' and M-x `gptel-mcp-disconnect', OR
+;; - Use gptel's tools menu, M-x `gptel-tools', OR
+;; - Access tools from `gptel-menu'
+
+;;; Code:
+(require 'gptel)
+(require 'cl-lib)
+(eval-when-compile (require 'transient))
+
+;;;; MCP integration - requires the mcp package
+(declare-function mcp-hub-get-all-tool "mcp-hub")
+(declare-function mcp-hub-get-servers "mcp-hub")
+(declare-function mcp-hub "mcp-hub")
+(defvar mcp-hub-servers)
+
+(defun gptel-mcp-connect (&optional interactive)
+  "Get gptel tools from MCP servers using the mcp package.
+
+If INTERACTIVE is non-nil, guide the user through setting up mcp and
+query for servers to retrieve tools from."
+  (interactive (list t))
+  (if (locate-library "mcp-hub")
+      (unless (require 'mcp-hub nil t)
+        (user-error "Could not load `mcp-hub'!  Please install\
+ or configure the mcp package"))
+    (user-error "Could not find mcp!  Please install or configure the mcp 
package"))
+  (if (null mcp-hub-servers)
+      (user-error "No MCP servers available!  Please configure 
`mcp-hub-servers'")
+    (let* ((servers (mcp-hub-get-servers))
+           (active (cl-remove-if-not (lambda (el) (eq (plist-get el :status) 
'connected))
+                                     servers)))
+      (if (null active)
+          (when (and interactive
+                     (y-or-n-p "No MCP servers are running.  Open the MCP 
hub?"))
+            (message (substitute-command-keys
+                      "Start some MCP servers for gptel to connect to!\
+ (\\`s' to start, \\`k' to kill, \\[mcp-hub] to get here)"))
+            (mcp-hub))
+        ;; Check which servers to connect to
+        (letrec ((tools (mcp-hub-get-all-tool :asyncp t :categoryp t))
+                 (connect-all-fn
+                  (lambda () (mapc #'(lambda (tool) (apply #'gptel-make-tool 
tool))
+                              tools)
+                    (message "Added %d tools from %d MCP server%s"
+                             (length tools) (length active)
+                             (if (= (length active) 1) "" "s")))))
+          (if (not interactive)
+              (funcall connect-all-fn)  ; Connect to all of them
+            (when-let* ((names (completing-read-multiple ; Ask for confirmation
+                                "Get tools from servers (separate with \",\"): 
"
+                                (cons "ALL" (mapcar (lambda (el) (plist-get el 
:name)) active))
+                                nil t)))
+              (if (member "ALL" names)
+                  (funcall connect-all-fn)
+                (let ((idx 0))
+                  (dolist (name names)
+                    (mapc (lambda (tool)
+                            (when (equal (plist-get tool :category) (format 
"mcp-%s" name))
+                              (apply #'gptel-make-tool tool)
+                              (cl-incf idx)))
+                          tools))
+                  (message "Added %d tools from MCP servers: %S"
+                           idx names))))))))))
+
+(defun gptel-mcp-disconnect (&optional interactive)
+  "Unregister gptel tools provided by MCP servers using the mcp package.
+
+If INTERACTIVE is non-nil, query the user about which tools to remove."
+  (interactive (list t))
+  (if-let* ((names-alist
+             (cl-loop
+              for (category . _tools) in gptel--known-tools
+              if (string-match "^mcp-\\(.*\\)" category)
+              collect (cons (match-string 1 category) category))))
+      (let ((remove-fn (lambda (cat-names)
+                         (mapc (lambda (category) (setf (alist-get category 
gptel--known-tools
+                                                              nil t #'equal)
+                                                   nil))
+                               cat-names))))
+        (if interactive
+            (when-let* ((categories
+                         (completing-read-multiple
+                          "Remove MCP server tools for (separate with \",\"): "
+                          (cons '("ALL" . nil) names-alist)
+                          nil t)))
+              (if (member "ALL" categories)
+                  (setq categories (map-values names-alist))
+                (setq categories (mapcar (lambda (n) (cdr (assoc n 
names-alist))) categories)))
+              (funcall remove-fn categories)
+              (message "Removed MCP tools for: %S" (map-keys names-alist)))
+          (funcall remove-fn (map-values names-alist))))
+    (message "No MCP tools found!")))
+
+(with-eval-after-load 'gptel-transient
+  ;; FIXME: If `gptel-mcp-connect' opens mcp-hub, the transient stays open.  I
+  ;; don't know how to fix this.
+  (transient-define-suffix gptel--suffix-mcp-connect ()
+    "Register tools provided by MCP servers."
+    :key "M+"
+    :description "Add tools from MCP servers"
+    :transient t
+    (interactive)
+    (condition-case err
+        (call-interactively #'gptel-mcp-connect)
+      (user-error (message "%s" (cadr err))))
+    (transient-setup))
+
+  (transient-define-suffix gptel--suffix-mcp-disconnect ()
+    "Remove tools provided by MCP servers from gptel."
+    :key "M-"
+    :description "Remove tools from MCP servers"
+    :transient t
+    :inapt-if (lambda () (or (not (boundp 'mcp-hub-servers))
+                        (null mcp-hub-servers)))
+    (interactive)
+    (call-interactively #'gptel-mcp-disconnect)
+    (transient-setup))
+
+  (transient-remove-suffix 'gptel-tools '(0 2))
+  (transient-append-suffix 'gptel-tools '(0 -1)
+    [""
+     (gptel--suffix-mcp-connect)
+     (gptel--suffix-mcp-disconnect)]))
+
+(provide 'gptel-integrations)
+;;; gptel-integrations.el ends here

Reply via email to