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

    gptel-integrations: Start MCP servers automatically
    
    We try to integrate MCP servers via mcp into gptel using only two
    options:
    
    1. A command to add tools from MCP servers to gptel, starting
    servers if required.  This is `gptel-mcp-connect', or M+ in the
    tools menu.
    
    2. M- to remove MCP tools from gptel.  Calling it again will shut
    down MCP servers.  This is `gptel-mcp-disconnect', or M- in the
    tools menu.
    
    * gptel-transient.el (gptel-menu): Show the toos menu entry if mcp
    support is enabled (via gptel-integrations), even if no tools are defined.
    
    * gptel-integrations.el (gptel-mcp-disconnect, gptel-mcp-connect)
    (gptel-mcp--activate-tools): When adding tools from mcp, try to
    connect to MCP servers automatically.  Try to message the user if
    any servers fail to load.  Make `gptel-mcp-disconnect' inapt if
    there is nothing to do.  Silence byte-compile warnings about
    functions being unavailable at runtime -- it ignores
    `with-eval-after-load'.
---
 gptel-integrations.el | 171 ++++++++++++++++++++++++++++++++++----------------
 gptel-transient.el    |   3 +-
 2 files changed, 118 insertions(+), 56 deletions(-)

diff --git a/gptel-integrations.el b/gptel-integrations.el
index 6f10eb13aa..4f0c712b1a 100644
--- a/gptel-integrations.el
+++ b/gptel-integrations.el
@@ -39,14 +39,20 @@
 ;;;; 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-start-all-server "mcp-hub")
+(declare-function mcp-stop-server "mcp")
 (declare-function mcp-hub "mcp-hub")
 (defvar mcp-hub-servers)
+(defvar mcp-server-connections)
 
-(defun gptel-mcp-connect (&optional interactive)
-  "Get gptel tools from MCP servers using the mcp package.
+(defun gptel-mcp-connect (&optional interactive server-callback)
+  "Add 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."
+MCP servers are started if required.  If INTERACTIVE is non-nil (or
+called interactively), guide the user through setting up mcp, and query
+for servers to retrieve tools from.
+
+Call SERVER-CALLBACK after starting MCP servers, if starting them."
   (interactive (list t))
   (if (locate-library "mcp-hub")
       (unless (require 'mcp-hub nil t)
@@ -55,41 +61,53 @@ query for servers to retrieve tools from."
     (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))))))))))
+    (let ((unregistered-servers ;Available servers minus servers already 
registered with gptel
+           (cl-loop for server in mcp-hub-servers
+                    with registered-names =
+                    (cl-loop for (cat . _tools) in gptel--known-tools
+                             if (string-prefix-p "mcp-" cat)
+                             collect (substring cat 4))
+                    unless (member (car server) registered-names)
+                    collect server)))
+      (if unregistered-servers
+          (let* ((servers
+                  (if interactive
+                      (let ((picks
+                             (completing-read-multiple
+                              "Add tools from MCP servers (separate with 
\",\"): "
+                              (cons '("ALL") unregistered-servers) nil t)))
+                        (if (member "ALL" picks)
+                            unregistered-servers
+                          (mapcar (lambda (s) (assoc s mcp-hub-servers)) 
picks)))
+                    unregistered-servers))
+                 (server-active-p
+                  (lambda (server) (gethash (car server) 
mcp-server-connections)))
+                 (get-all-tools (lambda () (mcp-hub-get-all-tool :asyncp t 
:categoryp t)))
+                 (inactive-servers (cl-remove-if server-active-p servers))
+                 (add-all-tools
+                  (lambda ()
+                    "Register and add tools from servers.  Report failures."
+                    (let ((tools (funcall get-all-tools))
+                          (now-active (cl-remove-if-not server-active-p 
mcp-hub-servers)))
+                      (mapc (lambda (tool) (apply #'gptel-make-tool tool)) 
tools)
+                      (gptel-mcp--activate-tools tools)
+                      (if-let* ((failed (cl-set-difference inactive-servers 
now-active
+                                                           :test #'equal)))
+                          (message "%d/%d server%s failed to start: %s.  Run 
\\[mcp-hub] to investigate."
+                                   (length failed) (length inactive-servers)
+                                   (if (= (length failed) 1) "" "s")
+                                   (mapconcat #'car failed ", "))
+                        (message "Added %d tools from %d MCP server%s: %s"
+                                 (length tools) (length now-active)
+                                 (if (= (length now-active) 1) "" "s")
+                                 (mapconcat #'car now-active ", ")))
+                      (when (functionp server-callback) (funcall 
server-callback))))))
+
+            (if inactive-servers        ;start servers
+                (mcp-hub-start-all-server
+                 add-all-tools (mapcar #'car inactive-servers))
+              (funcall add-all-tools)))
+        (message "All MCP tools are already available to gptel!")))))
 
 (defun gptel-mcp-disconnect (&optional interactive)
   "Unregister gptel tools provided by MCP servers using the mcp package.
@@ -102,23 +120,51 @@ If INTERACTIVE is non-nil, query the user about which 
tools to remove."
               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))
+                         (setq gptel-tools ;Remove from gptel-tools
+                          (cl-delete-if (lambda (tool) (member 
(gptel-tool-category tool)
+                                                          cat-names))
+                                        gptel-tools))
+                         (mapc (lambda (category) ;Remove from registry
+                                 (setf (alist-get category gptel--known-tools
+                                                  nil t #'equal)
+                                       nil))
                                cat-names))))
         (if interactive
-            (when-let* ((categories
+            (when-let* ((server-names
                          (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!")))
+              (when (member "ALL" server-names)
+                  (setq server-names (mapcar #'car names-alist)))
+              (funcall remove-fn        ;remove selected tool categories
+                       (mapcar (lambda (s) (cdr (assoc s names-alist))) 
server-names))
+              (if (y-or-n-p
+                   (format "Removed MCP tools from %d server%s.  Also shut 
down MCP servers?"
+                           (length server-names)
+                           (if (= (length server-names) 1) "" "s")))
+                  (progn (mapc #'mcp-stop-server server-names)
+                         (message "Shut down MCP servers: %S" server-names))
+                (message "Removed MCP tools for: %S" server-names)))
+          (funcall remove-fn (mapcar #'cdr names-alist))))
+    ;; No MCP tools, ask to shut down servers
+    (if (cl-loop
+         for v being the hash-values of mcp-server-connections
+         never v)
+        (when interactive (message "No MCP servers active!"))
+      (when (or (not interactive)
+                (y-or-n-p "No MCP tools in gptel!  Shut down all MCP servers? 
"))
+        (dolist (server mcp-hub-servers)
+          (when (gethash (car server) mcp-server-connections)
+            (mcp-stop-server (car server))))))))
+
+(defun gptel-mcp--activate-tools (&optional tools)
+  "Activate TOOLS or all MCP tools in current gptel session."
+  (unless tools (setq tools (mcp-hub-get-all-tool :asyncp t :categoryp t)))
+  (dolist (tool tools)
+    (cl-pushnew (gptel-get-tool (list (plist-get tool :category)
+                                      (plist-get tool :name)))
+                gptel-tools)))
 
 (with-eval-after-load 'gptel-transient
   ;; FIXME: If `gptel-mcp-connect' opens mcp-hub, the transient stays open.  I
@@ -126,21 +172,32 @@ If INTERACTIVE is non-nil, query the user about which 
tools to remove."
   (transient-define-suffix gptel--suffix-mcp-connect ()
     "Register tools provided by MCP servers."
     :key "M+"
-    :description "Add tools from MCP servers"
+    :description "Add MCP server tools"
     :transient t
     (interactive)
     (condition-case err
-        (call-interactively #'gptel-mcp-connect)
+        (gptel-mcp-connect
+         t (lambda () (when-let* ((transient--prefix)
+                             ((eq (oref transient--prefix command)
+                                  'gptel-tools)))
+                   (transient-setup 'gptel-tools))))
       (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"
+    :description (lambda () (if (cl-some (lambda (cat) (string-match-p "^mcp-" 
cat))
+                                    (map-keys gptel--known-tools))
+                           "Remove MCP server tools"
+                         "Shut down MCP servers"))
     :transient t
-    :inapt-if (lambda () (or (not (boundp 'mcp-hub-servers))
-                        (null mcp-hub-servers)))
+    :inapt-if
+    (lambda () (or (not (boundp 'mcp-hub-servers))
+              (null mcp-hub-servers)
+              (cl-loop
+               for v being the hash-values of mcp-server-connections
+               never v)))
     (interactive)
     (call-interactively #'gptel-mcp-disconnect)
     (transient-setup))
@@ -153,3 +210,7 @@ If INTERACTIVE is non-nil, query the user about which tools 
to remove."
 
 (provide 'gptel-integrations)
 ;;; gptel-integrations.el ends here
+
+;; Local Variables:
+;; byte-compile-warnings: (not noruntime)
+;; End:
diff --git a/gptel-transient.el b/gptel-transient.el
index 2425a392ca..8cf6a72b21 100644
--- a/gptel-transient.el
+++ b/gptel-transient.el
@@ -577,7 +577,8 @@ Also format its value in the Transient menu."
     (gptel--infix-context-remove-all)
     (gptel--suffix-context-buffer)]
    [:pad-keys t
-    :if (lambda () (and gptel-use-tools gptel--known-tools))
+    :if (lambda () (and gptel-use-tools
+                   (or gptel--known-tools (featurep 'gptel-integrations))))
     "" (:info
         (lambda ()
           (concat

Reply via email to