🚀 让 AI 与 Emacs 深度集成 - 推荐试用 mcp.el 管理 MCP 服务器

:rocket: 让 AI 与 Emacs 深度集成 - 推荐试用 mcp.el 管理 MCP 服务器

目前已经实现官方协议的基本功能。


:star2: 为什么需要 mcp.el?

在本地化 AI 应用场景中,我们经常需要同时管理多个 MCP 服务实例。mcp.el 提供了一套完整的解决方案:

  • 一键式 服务管理(启动/停止/重启)
  • 可视化 服务状态监控
  • 无缝集成 gptel、llm 等流行 AI 包
  • 灵活扩展 的服务器配置体系

:hammer_and_wrench: 快速上手

基础配置示例

(require 'mcp-hub)
(setq mcp-hub-servers
      '(("filesystem" "npx" ("-y" "@modelcontextprotocol/server-filesystem" "/path/example/"))
        ("everything" "npx" ("-y" "@modelcontextprotocol/server-everything"))))

然后使用 mcp-hub 启动管理界面(会自动启动所有的 mcp server)或者使用 mcp-hub-start-all-server 手动启动所有 mcp 服务器。

Mcp 服务器管理界面


▲ 通过直观界面管理服务生命周期


:fire: 核心特性

  • 与MCP服务器的结构化通信
  • 支持文件系统和通用MCP服务器
  • 可扩展的工具和提示系统
  • 异步和同步操作
  • 资源管理功能
  • 管理服务器生命周期的直观界面(启动/停止/重启)
  • 集成流行的Emacs包(例如gptel、llm)

:dart: 未来路线

  • 服务依赖关系管理
  • 实现 mcp 协议客户端全部特性
  • 支持 http 的 mcp 服务器连接
  • 与 Emacs 中的其他 ai 客户端更加简单的集成
  • 性能优化指南
  • 中文文档完善

欢迎大家多多试用,探索 mcp 与 Emacs 的有机结合!

11 个赞

你好! 看起来很exciting, 但对外行而言有点不太明白. 能给出一个具体的例子, 最好是和gptel结合, step by step, 来解决一个实际的问题吗? 谢谢!

配置

首先需要配置好 gptel 有可以使用工具调用的 ai 模型, 比如 claude 之类的。 然后配置想要使用的 mcp server, 我拿 fetch 这个 mcp server 来举例,这个 mcp server 可以获取网页内容,并且转化为 markdown。

根据官方文档配置想要启动的服务器

这个配置和官方的一样,第一个参数是 mcp server 的名字,第二个参数是 command, 最后一个参数是 args

(require 'mcp-hub)
(setq mcp-hub-servers
      '(("fetch" "uvx" ("mcp-server-fetch"))))

启动

然后使用命令 mcp-hub 启动服务器管理界面,或者也可以用 mcp-hub-start-all-server 来启动所有的配置好的 mcp 服务器。

可以看到 fetch 这个 mcp server 连接成功。

注册工具

目前想要与 gptel 结合,可以用以下几个函数。 这个函数是让 mcp 生成符合 gptel 工具格式的列表,并且注册到 gptel 里面,这样 gptel 就知道可以调用那些 工具。

(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)))

运行完这个函数,就可以在 gptel 的工具使用菜单中看到注册好的 mcp 工具。

激活工具

目前有两种方式。 第一种:使用 gptel 自带的 gptel-menu 里面的菜单激活工具。 第二种:使用以下函数激活所有所有 mcp 工具和 关闭所有 mcp 工具。(之后打算写一个单独的插件来处理与 gptel 交互)

;; 激活所有 mcp 工具
(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)))

;; 关闭所有 mcp 工具
(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)))

激活工具之后可以看到 fetch 工具被激活。

使用工具

激活之后就正常和 ai 对话就好了,在这个过程中 ai 自己决定是否要使用工具来获取信息。

4 个赞

可以像这样子安装 MCP server 吗?

以及,有人弄了一个在线的 MCP 路由,这个可以支持吗?

现在目前还不支持, 它需要支持 sse 传输,目前 mcp.el 只能使用 stdio。

目前可以看看这个 mcp-proxy, 这个可以将 stdio 转换成 sse, 用套一层的方式尝试一下。

1 个赞

想要实现自动安装可以使用和 mcphub.nvim 一样的方案,使用 mcp-hub 来管理所有 mcp server 的安装,连接和交互。

或者自己实现一套,基于 mcp-marketplace 来实现一套市场机制。

目前 mcp-hub.el 只能是简单管理 server 的启动和关闭。

1 个赞

Anthropic 昨晚发布 MCP 重大更新

移除了 /sse 端点 所有客户端→服务器消息通过 /message(或类似)端点传输

消除了对高可用长连接的需求 无需 SSE 即可在普通 HTTP 服务器中实现 MCP 服务器可根据需要选择使用 SSE 进行流式响应 “只是 HTTP”,确保与中间件和基础设施兼容

这标题党+机翻完全看不懂,还是别随意转发未经自己处理的消息比较好。

这里是实际的 PR,目前还是草案,离实际跟进还有段时间。

4 个赞

@lizqwer
大佬你好, 请问一下这个怎么配合 gptel 使用呢?
我已经看到了 Mcp-Hub buffer中已经 connected 了, 我也进行了 register-tool, 也在 gptel-tools 中启用
当我进行使用时返回了错误内容:

Gemini error: (HTTP/1.1 400 Bad Request) Invalid value at 'tools[0].function_declarations[2].parameters.properties[2].value.items' (required), Starting an object on a scalar field
Invalid value at 'tools[0].function_declarations[9].parameters.properties[0].value.items' (required), Starting an object on a scalar field

环境:
gptel => 0.9.8
Model: gemini-2.0-flash-exp (已知这个模型在gptel buffer 和 vscode 中的 cline 中的 mcp-server 正常使用)
Eamcs : 30.1 (正式版)
OS: Windows 10
工具: filesystem
我该怎么排除问题? 是 gptel 问题吗?
gptel-log 调试信息:

{
  "gptel": "request body",
  "timestamp": "2025-03-19 16:47:25"
}
{
  "contents": [
    {
      "role": "user",
      "parts": [
        {
          "text": "Hello"
        }
      ]
    }
  ],
  "safetySettings": [
    {
      "category": "HARM_CATEGORY_HARASSMENT",
      "threshold": "BLOCK_NONE"
    },
    {
      "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
      "threshold": "BLOCK_NONE"
    },
    {
      "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
      "threshold": "BLOCK_NONE"
    },
    {
      "category": "HARM_CATEGORY_HATE_SPEECH",
      "threshold": "BLOCK_NONE"
    }
  ],
  "system_instruction": {
    "parts": {
      "text": "You are a large language model and a conversation partner. Respond concisely."
    }
  },
  "tools": [
    {
      "function_declarations": [
        {
          "name": "list_allowed_directories",
          "description": "Returns the list of directories that this server is allowed to access. Use this to understand which directories are available before trying to access files.",
          "parameters": null
        },
        {
          "name": "get_file_info",
          "description": "Retrieve detailed metadata about a file or directory. Returns comprehensive information including size, creation time, last modified time, permissions, and type. This tool is perfect for understanding file characteristics without reading the actual content. Only works within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": ""
              }
            },
            "required": [
              "path"
            ]
          }
        },
        {
          "name": "search_files",
          "description": "Recursively search for files and directories matching a pattern. Searches through all subdirectories from the starting path. The search is case-insensitive and matches partial names. Returns full paths to all matching items. Great for finding files when you don't know their exact location. Only searches within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": ""
              },
              "pattern": {
                "type": "string",
                "description": ""
              },
              "excludePatterns": {
                "type": "array",
                "description": "",
                "items": {
                  "type": "string",
                  "properties": {},
                  "required": {}
                }
              }
            },
            "required": [
              "path",
              "pattern"
            ]
          }
        },
        {
          "name": "move_file",
          "description": "Move or rename files and directories. Can move files between directories and rename them in a single operation. If the destination exists, the operation will fail. Works across different directories and can be used for simple renaming within the same directory. Both source and destination must be within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "source": {
                "type": "string",
                "description": ""
              },
              "destination": {
                "type": "string",
                "description": ""
              }
            },
            "required": [
              "source",
              "destination"
            ]
          }
        },
        {
          "name": "directory_tree",
          "description": "Get a recursive tree view of files and directories as a JSON structure. Each entry includes 'name', 'type' (file/directory), and 'children' for directories. Files have no children array, while directories always have a children array (which may be empty). The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": ""
              }
            },
            "required": [
              "path"
            ]
          }
        },
        {
          "name": "list_directory",
          "description": "Get a detailed listing of all files and directories in a specified path. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. This tool is essential for understanding directory structure and finding specific files within a directory. Only works within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": ""
              }
            },
            "required": [
              "path"
            ]
          }
        },
        {
          "name": "create_directory",
          "description": "Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. If the directory already exists, this operation will succeed silently. Perfect for setting up directory structures for projects or ensuring required paths exist. Only works within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": ""
              }
            },
            "required": [
              "path"
            ]
          }
        },
        {
          "name": "edit_file",
          "description": "Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Returns a git-style diff showing the changes made. Only works within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": ""
              },
              "edits": {
                "type": "array",
                "description": "",
                "items": {
                  "type": "object",
                  "properties": {
                    "oldText": {
                      "type": "string",
                      "description": "Text to search for - must match exactly"
                    },
                    "newText": {
                      "type": "string",
                      "description": "Text to replace with"
                    }
                  },
                  "required": [
                    "oldText",
                    "newText"
                  ]
                }
              },
              "dryRun": {
                "type": "boolean",
                "description": "Preview changes using git-style diff format"
              }
            },
            "required": [
              "path",
              "edits"
            ]
          }
        },
        {
          "name": "write_file",
          "description": "Create a new file or completely overwrite an existing file with new content. Use with caution as it will overwrite existing files without warning. Handles text content with proper encoding. Only works within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": ""
              },
              "content": {
                "type": "string",
                "description": ""
              }
            },
            "required": [
              "path",
              "content"
            ]
          }
        },
        {
          "name": "read_multiple_files",
          "description": "Read the contents of multiple files simultaneously. This is more efficient than reading files one by one when you need to analyze or compare multiple files. Each file's content is returned with its path as a reference. Failed reads for individual files won't stop the entire operation. Only works within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "paths": {
                "type": "array",
                "description": "",
                "items": {
                  "type": "string",
                  "properties": {},
                  "required": {}
                }
              }
            },
            "required": [
              "paths"
            ]
          }
        },
        {
          "name": "read_file",
          "description": "Read the complete contents of a file from the file system. Handles various text encodings and provides detailed error messages if the file cannot be read. Use this tool when you need to examine the contents of a single file. Only works within allowed directories.",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": ""
              }
            },
            "required": [
              "path"
            ]
          }
        }
      ]
    }
  ],
  "generationConfig": {
    "temperature": 1.0
  }
}
{
  "gptel": "response body",
  "timestamp": "2025-03-19 16:47:27"
}

{
  "error": {
    "code": 400,
    "message": "Invalid value at 'tools[0].function_declarations[2].parameters.properties[2].value.items' (required), Starting an object on a scalar field\nInvalid value at 'tools[0].function_declarations[9].parameters.properties[0].value.items' (required), Starting an object on a scalar field",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "tools[0].function_declarations[2].parameters.properties[2].value.items",
            "description": "Invalid value at 'tools[0].function_declarations[2].parameters.properties[2].value.items' (required), Starting an object on a scalar field"
          },
          {
            "field": "tools[0].function_declarations[9].parameters.properties[0].value.items",
            "description": "Invalid value at 'tools[0].function_declarations[9].parameters.properties[0].value.items' (required), Starting an object on a scalar field"
          }
        ]
      }
    ]
  }
}

在 server 管理界面里面 按 l 键可以查看与 server 交互的日志,可以发出来看一下

当我进行 gptel 发送的时候, 貌似 log 没有变化.
以下是 mcp-hub log

[jsonrpc] e[18:36:48.592] --> initialize[1] {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"mcp-emacs","version":"0.1.0"}}}
[stderr]  Secure MCP Filesystem Server running on stdio
[jsonrpc] e[18:36:50.360] <-- initialize[1] 
[jsonrpc] e[18:36:50.362] --> notifications/initialized {"jsonrpc":"2.0","method":"notifications/initialized"}
[jsonrpc] e[18:36:50.362] --> tools/list[2] {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"curosr":null}}
[stderr]  Allowed directories: [ 'C:\\Users\\Jack\\Desktop\\Test' ]
[jsonrpc] e[18:36:50.366] <-- tools/list[2] 

我注意到 reddit 的某个用户使用 claude 模型进行 filesystem-server 也有类似问题…
https://www.reddit.com/r/emacs/comments/1jcmop0/comment/mi5gf16/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

我发现是 filesystem 这个 mcp server 里面的 search_files 工具的 excludePatterns 参数描述生成有问题。 尝试修复了一下, 查看这个 更改

请尝试一下是否解决问题

filesystem
你更新之后可以用了

1 个赞

已经上官方列表啦哈哈哈

现在 mcp.el 也能连接 sse 了,我在这个 分支 添加了 SSE 的支持。

目前只用 qdrant 这个 mcp server 做了测试

1 个赞

轻轻的用了(实际是随便玩玩)几天, 还没感觉到 MCP 有什么好用的玩法(或者厉害的玩法)…
也可能是我想法不够新颖, 玩不出花样

1 个赞

目前我用tools最有用的一个就是,可以适时读取内置包或者手册或者第三方包源码,这样可以节省上下文,同时可以让 AI 帮忙多个包相互参照,实现自己想要的功能。

问一下大伙儿:Aidermacs有必要集成mcp.el吗,有什么直接用途吗?