Calling MCP tools from Emacs

Posted on Apr 10, 2025

A few months ago I daydreamed bridging Emacs LLMs with a burgeoning ecosystem of MCP tools: being able to use websearch, run Python code within a local virtualenv, or anything off the awesome list. The Model Context Protocol (MCP) provides a protocol for integrating tools with LLM’s. Designed by Anthropic for Claude and picked up by OpenAI, it’s shaping up to be the standard for wiring up tools.

lizqwerscott/mcp.el implements an MCP client from within Emacs alongside an interface for managing connected MCP servers. Mix in a few glue functions to wire the MCP servers to karthink/gptel as gpt-tools and your Emacs LLM now has [potentially dangerous] superpowers.

An Example: Fetching and Executing

Here’s an example of gptel using MCP tools to 1) fetch a website (example.org), and then 2) to evaluate Python code that count the number of words rendered.

Tool-use certainly slows things down as the LLM defers to execute some task, but the overhead doesn’t feel higher than hosted services.

/*** Hello! Can you fetch https://example.org and tell me how many words are rendered to the user?

I'll fetch the webpage for you and count the words that would be rendered to a user.

#+begin_tool (fetch :url "https://example.org")
(:name "fetch" :args (:url "https://example.org"))

Contents of https://example.org/:
This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.

[More information...](https://www.iana.org/domains/example)
#+end_tool
Now I'll count the words in the rendered content:

#+begin_tool (run_python_code :python_code "# Count the words ...)
(:name "run_python_code" :args (:python_code "# Count the words in the rendered content
content = \"\"\"This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.

More information...\"\"\"

# Split by whitespace and count non-empty words
words = [word for word in content.split() if word.strip()]
print(f\"Number of words rendered to the user: {len(words)}\")"))

<status>success</status>
<output>
Number of words rendered to the user: 26
</output>
#+end_tool
The example.org webpage renders 26 words to the user. This includes the text content and the "More information..." link text, but excludes any HTML markup or non-visible elements.

The use-package config

Here’s the config implemented via jwiegley/use-package and radian-software/straight.el. A warning: the glue functions are likely to be superseded in an upstream library.

This was tested against Emacs 30.

(use-package mcp
  :straight (:host github :repo "lizqwerscott/mcp.el")

  :custom
  (mcp-hub-servers
   '(("fetch" . (:command "uvx" :args ("mcp-server-fetch")))))

  :config
  (defun gptel-mcp-register-tool ()
    (interactive)
    (let ((tools (mcp-hub-get-all-tool :asyncp t :categoryp t)))
      (mapcar #'(lambda (tool)
                  (apply #'gptel-make-tool
                         tool))
              tools)))

  (defun gptel-mcp-use-tool ()
    (interactive)
    (let ((tools (mcp-hub-get-all-tool :asyncp t :categoryp t)))
      (mapcar #'(lambda (tool)
                  (let ((path (list (plist-get tool :category)
                                    (plist-get tool :name))))
                    (push (gptel-get-tool path)
                          gptel-tools)))
              tools)))

  (defun gptel-mcp-close-use-tool ()
    (interactive)
    (let ((tools (mcp-hub-get-all-tool :asyncp t :categoryp t)))
      (mapcar #'(lambda (tool)
                  (let ((path (list (plist-get tool :category)
                                    (plist-get tool :name))))
                    (setq gptel-tools
                          (cl-remove-if #'(lambda (tool)
                                            (equal path
                                                   (list (gptel-tool-category tool)
                                                         (gptel-tool-name tool))))
                                        gptel-tools))))
              tools)))

  (defun my-gptel-mcp-start ()
    (interactive)
    (mcp-hub-start-all-server)
    (gptel-mcp-register-tool)
    (setq gptel-tools '())
    (gptel-mcp-use-tool)))

Improvements I’d like to see

  • extend the mcp.el hub interface showing if gptel is connected per tool
  • improved logging when MCP server is being installed