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