Calling MCP tools from Emacs
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