Merge branch 'feat/mcp_server_with_eim_v6.0' into 'release/v6.0'

feat(tools): Fixed mcp-server functionality + EIM integration (v6.0)

See merge request espressif/esp-idf!46481
This commit is contained in:
Marius Vikhammer
2026-03-17 10:14:31 +08:00
3 changed files with 264 additions and 154 deletions
+71 -19
View File
@@ -267,44 +267,96 @@ Read Otadata Partition: ``read-otadata``
This command prints the contents of the ``otadata`` partition which stores the information about the currently selected OTA app slot. Refer to :doc:`/api-reference/system/ota` for more about the ``otadata`` partition.
Start MCP Server: ``mcp-server``
---------------------------------
ESP-IDF MCP Server
-------------------
The ESP-IDF MCP (Model Context Protocol) server enables AI integration with ESP-IDF projects. The MCP server provides tools and resources that allow AI assistants to interact with your ESP-IDF project through a standardized protocol. Using natural language, you can tell the AI assistant commands like "set target to esp32" or "build this project".
Starting the MCP Server
^^^^^^^^^^^^^^^^^^^^^^^^
To use the MCP server with an AI assistant, configure your agent or IDE to start the server. You can do that in two ways:
1. Using ``eim run`` (recommended): Use the ESP-IDF Installation Manager (EIM) to start a new process with an active ESP-IDF environment. This feature is available from EIM 0.8.1 and **ESP-IDF must be installed via the EIM installer**. This is the easiest option and does not require you to activate ESP-IDF in your shell first.
.. code-block:: bash
eim run "idf.py mcp-server"
2. Using ``idf.py`` directly: Run the MCP server with ``idf.py mcp-server`` from a shell where the ESP-IDF environment is already activated. The command must be executed from a valid ESP-IDF project directory, or use ``idf.py -C <project_dir> mcp-server`` to specify the project.
.. code-block:: bash
idf.py mcp-server
This command starts an MCP (Model Context Protocol) server that enables AI integration with ESP-IDF projects. The MCP server provides tools and resources that allow AI assistants to interact with your ESP-IDF project through a standardized protocol.
.. note::
The MCP server provides the following tools:
The MCP server requires the ``mcp`` feature to be installed. Install it using the EIM installer. See `EIM documentation > CLI Configuration - Global features <https://docs.espressif.com/projects/idf-im-ui/en/latest/cli_configuration.html#global-features-all-versions>`_ for how to install specific features.
- ``build_project``: Build the ESP-IDF project with specified target
- ``set_target``: Set the ESP-IDF target (esp32, esp32s3, esp32c6, etc.)
- ``flash_project``: Flash the built project to a connected device
- ``monitor_serial``: Start serial monitor (runs in background)
- ``clean_project``: Clean build artifacts
- ``menuconfig``: Open menuconfig interface (terminal-based)
Available Tools and Resources
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The MCP server provides the following commands you can use:
- ``set target``: Set the ESP-IDF target (esp32, esp32s3, esp32c6, etc.)
- ``build project``: Build the ESP-IDF project with the current target
- ``flash project``: Flash the built project to a connected device. Specify it by port name.
- ``clean project``: Clean build artifacts
The MCP server also provides these resources:
- ``project://config``: Get current project configuration
- ``project://status``: Get current project build status
- ``project://devices``: Get list of connected ESP devices
- ``project://status``: Get current project build status and artifacts
- ``project://devices``: Get list of connected devices
.. note::
Adding ESP-IDF MCP Server to IDEs and AI agents
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The MCP server requires the ``mcp`` Python package to be installed. Install it with: ``./install.sh --enable-mcp``.
Cursor IDE
~~~~~~~~~~
Adding ESP-IDF MCP Server to IDEs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add the ESP-IDF MCP server configuration to your Cursor ``mcp.json`` file:
**Claude Desktop:**
.. code-block:: json
Use the Claude CLI to add the ESP-IDF MCP server:
{
"mcpServers": {
"esp-idf-eim": {
"command": "eim",
"args": [
"run",
"idf.py mcp-server"
],
"env": {
"IDF_MCP_WORKSPACE_FOLDER": "${workspaceFolder}"
}
}
}
}
To use the ESP-IDF MCP server with Cursor IDE, open your ESP-IDF project folder and use the AI chat window. The AI assistant will have access to ESP-IDF specific tools and can help you build, flash, and manage your project.
The ``IDF_MCP_WORKSPACE_FOLDER`` environment variable tells the MCP server which directory contains your ESP-IDF project. This ensures the server operates in the correct project context, allowing it to access your project's configuration, build files, and perform operations like building and flashing in the right location.
Claude Desktop
~~~~~~~~~~~~~~
Use the Claude CLI to add the ESP-IDF MCP server.
With ``eim`` (no need to activate ESP-IDF first):
.. code-block:: bash
claude mcp add esp-idf python /path/to/esp-idf/tools/idf.py mcp-server --env IDF_PATH=/path/to/esp-idf
claude mcp add --transport stdio esp-idf-eim -- eim run "idf.py mcp-server"
With ``idf.py`` (must be executed from an activated ESP-IDF environment):
.. code-block:: bash
claude mcp add --transport stdio esp-idf -- idf.py mcp-server
Navigate to your ESP-IDF project directory and run the ``claude`` command to chat with the AI assistant.
Configuration Presets: ``--preset``
====================================
+71 -19
View File
@@ -267,44 +267,96 @@ uf2 二进制文件也可以通过 :ref:`idf.py uf2 <generate-uf2-binary>` 生
此命令将打印 ``otadata`` 分区的内容,该分区存储当前所选 OTA 应用程序分区的信息。有关 ``otadata`` 分区的更多信息,请参阅 :doc:`/api-reference/system/ota`
启动 MCP 服务器``mcp-server``
---------------------------------
ESP-IDF MCP 服务器
------------------
ESP-IDF 的 MCPModel Context Protocol,模型上下文协议)服务器可实现 AI 与 ESP-IDF 项目的集成。该 MCP 服务器提供工具和资源,使 AI 助手能够通过标准化协议与 ESP-IDF 项目交互。通过自然语言,即可向 AI 助手下达诸如“设定目标芯片为 esp32”或“构建项目”之类的命令。
启动 MCP 服务器
^^^^^^^^^^^^^^^
要配合 AI 助手使用 MCP 服务器,可配置智能体或 IDE 启动该服务器。具体来说,有两种实现方式:
1. 使用 ``eim run`` (推荐):通过 ESP-IDF 安装管理器 (ESP-IDF Installation Manager, EIM) 在已激活的 ESP-IDF 环境中启动一个新进程。该功能需要 EIM 0.8.1 或更高版本,且 **ESP-IDF 必须通过 EIM 安装程序安装**。该方案是最简单的选项,且无需先在 shell 中激活 ESP-IDF 环境。
.. code-block:: bash
eim run "idf.py mcp-server"
2. 直接使用 ``idf.py``:在已激活 ESP-IDF 环境的 shell 中运行 ``idf.py mcp-server`` 命令启动 MCP 服务器。必须在有效的 ESP-IDF 项目目录中执行该命令,或者使用 ``idf.py -C <project_dir> mcp-server`` 指定项目路径。
.. code-block:: bash
idf.py mcp-server
此命令将启动 MCP(模型上下文协议)服务器,实现 AI 与 ESP-IDF 项目的集成。该服务器通过标准化协议提供工具和资源,使 AI 助手能够与 ESP-IDF 项目进行交互。
.. note::
MCP 服务器提供以下工具:
MCP 服务器需要通过 EIM 安装器来安装 ``mcp`` 功能。有关如何安装特定功能,请参见 `EIM 文档 > CLI 配置 - 全局功能 <https://docs.espressif.com/projects/idf-im-ui/en/latest/cli_configuration.html#global-features-all-versions>`_
- ``build_project``:使用指定目标芯片构建 ESP-IDF 项目
- ``set_target``:设置 ESP-IDF 目标芯片(esp32、esp32s3、esp32c6 等)
- ``flash_project``:将构建好的项目烧录至已连接设备
- ``monitor_serial``:启动串行监视器(在后台运行)
- ``clean_project``:清理构建产物
- ``menuconfig``:打开 menuconfig 界面(基于终端
可用工具与资源
^^^^^^^^^^^^^^
MCP 服务器提供以下可用的命令:
- ``set target``:设置 ESP-IDF 的目标芯片(esp32esp32s3esp32c6 等
- ``build project``:使用当前目标构建 ESP-IDF 项目
- ``flash project``:将已构建的项目烧录到已连接的设备,通过端口名称进行指定。
- ``clean project``:清理构建产物
同时提供以下资源:
- ``project://config``:获取当前项目配置
- ``project://status``:获取当前项目构建状态
- ``project://devices``:获取已连接的 ESP 设备列表
- ``project://status``:获取当前项目构建状态和构建产物
- ``project://devices``:获取已连接的设备列表
.. note::
将 ESP-IDF MCP 服务器添加至 IDE 和 AI 智能体
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
运行 MCP 服务器需提前安装 ``mcp`` Python 包。可通过以下命令安装:``./install.sh --enable-mcp``
Cursor IDE
~~~~~~~~~~
将 ESP-IDF MCP 服务器添加到 IDE
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
将 ESP-IDF MCP 服务器配置添加至 Cursor 的 ``mcp.json`` 文件中:
**Claude Desktop**
.. code-block:: json
使用 Claude CLI 添加 ESP-IDF MCP 服务器:
{
"mcpServers": {
"esp-idf-eim": {
"command": "eim",
"args": [
"run",
"idf.py mcp-server"
],
"env": {
"IDF_MCP_WORKSPACE_FOLDER": "${workspaceFolder}"
}
}
}
}
若想在 Cursor IDE 中使用 ESP-IDF MCP 服务器,请打开 ESP-IDF 项目文件夹并使用 AI 聊天窗口,AI 助手能够访问 ESP-IDF 专用工具,并帮助构建、烧录和管理项目。
MCP 服务器能够通过 ``IDF_MCP_WORKSPACE_FOLDER`` 环境变量确认哪个目录包含 ESP-IDF 项目,确保服务器在正确的项目上下文中运行,使其能够访问项目的配置和构建文件,并在正确的位置执行诸如构建和烧录之类的操作。
Claude 桌面版
~~~~~~~~~~~~~
使用 Claude CLI 添加 ESP-IDF MCP 服务器。
使用 ``eim`` (无需事先激活 ESP-IDF):
.. code-block:: bash
claude mcp add esp-idf python /path/to/esp-idf/tools/idf.py mcp-server --env IDF_PATH=/path/to/esp-idf
claude mcp add --transport stdio esp-idf-eim -- eim run "idf.py mcp-server"
使用 ``idf.py`` (必须在已激活的 ESP-IDF 环境中执行):
.. code-block:: bash
claude mcp add --transport stdio esp-idf -- idf.py mcp-server
前往 ESP-IDF 项目目录,并运行 ``claude`` 命令,与 AI 助手聊天。
配置预设:``--preset``
========================
+122 -116
View File
@@ -1,20 +1,29 @@
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
from click.core import Context
from idf_py_actions.errors import FatalError
from idf_py_actions.tools import PropertyDict
from idf_py_actions.tools import get_build_context
from idf_py_actions.tools import get_target
from idf_py_actions.tools import idf_version
try:
from idf_component_tools.build_system_tools import CMAKE_PROJECT_LINE
except ImportError:
CMAKE_PROJECT_LINE = [
r'include($ENV{IDF_PATH}/tools/cmake/project.cmake)',
r'include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)',
]
try:
from mcp.server.fastmcp import FastMCP
@@ -23,51 +32,98 @@ except ImportError:
MCP_AVAILABLE = False
def is_valid_project_dir(directory: str) -> bool:
"""
Determine if the given directory is a valid ESP-IDF project directory.
- Must be a directory.
- Must contain a CMakeLists.txt.
- CMakeLists.txt must include CMAKE_PROJECT_LINE.
"""
root = Path(directory)
if not root.is_dir():
return False
cmakelists_path = root / 'CMakeLists.txt'
if not cmakelists_path.is_file():
return False
try:
with open(str(cmakelists_path), encoding='utf-8') as f:
for line in f:
if any(proj_line in line for proj_line in CMAKE_PROJECT_LINE):
return True
except Exception:
return False
return False
def action_extensions(base_actions: dict, project_path: str) -> dict:
"""ESP-IDF MCP Server Extension"""
def start_mcp_server(action_name: str, ctx: Context, args: PropertyDict, **kwargs: Any) -> None:
"""Start MCP server for ESP-IDF project integration"""
if not MCP_AVAILABLE:
raise FatalError('MCP dependencies not available. Install with: ./install.sh --enable-mcp')
raise FatalError(
'MCP dependencies not available. '
'Install ESP-IDF using the EIM installer and select the "mcp" feature to be included. '
'For more information, refer to the official Espressif EIM Installer documentation '
'or use "idf.py docs" and search for EIM configuration instructions.'
)
current_project = None
# Use current working directory if available, fallback to project_path
for project_dir in [os.getcwd(), project_path]:
if project_dir and os.path.exists(os.path.join(project_dir, 'CMakeLists.txt')):
current_project = project_dir
break
if not current_project:
raise FatalError('Open the MCP server in a valid ESP-IDF project directory.')
print(f'Starting ESP-IDF MCP Server for project: {current_project}')
print(f'Target: {get_target(current_project)}')
print(f'ESP-IDF Version: {idf_version()}')
print(f'Working Directory: {os.getcwd()}')
# Verify that mcp-server was executed from a valid ESP-IDF project directory.
# This is necessary to obtain the correct context such as args, project path, etc.
if not is_valid_project_dir(project_path):
current_project = None
for candidate in [os.getcwd(), os.environ.get('IDF_MCP_WORKSPACE_FOLDER', '')]:
if is_valid_project_dir(candidate):
current_project = candidate
break
if not current_project:
raise FatalError('Open the MCP server in a valid ESP-IDF project directory.')
try:
cmd = [
sys.executable,
os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py'),
'-C',
current_project,
'mcp-server',
]
print(
f'Starting ESP-IDF MCP Server with command: {" ".join(cmd)} in project path: {current_project}',
file=sys.stderr,
)
subprocess.run(cmd, cwd=current_project, check=True)
return
except Exception as e:
print(f'ERROR: Failed to start ESP-IDF MCP Server: {str(e)}', file=sys.stderr)
raise FatalError(f'Failed to start ESP-IDF MCP Server: {str(e)}') from e
# Initialize MCP server
mcp = FastMCP('ESP-IDF')
# === TOOLS (Actions) ===
@mcp.tool()
def build_project(target: str = 'all') -> str:
"""Build ESP-IDF project with specified target"""
def build_project() -> str:
"""Build ESP-IDF project"""
try:
# Use the current project directory
cmd = [
sys.executable,
os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py'),
'-C',
current_project,
'build',
]
result = subprocess.run(cmd, capture_output=True, text=True, cwd=current_project)
# Information logs are shown in some mcp clients using stderr
print(f'INFO: Building project with command: {" ".join(cmd)} in path: {project_path}', file=sys.stderr)
result = subprocess.run(cmd, capture_output=True, text=True, cwd=project_path)
if result.returncode == 0:
return f'Successfully built target: {target}'
print('INFO: Build successful', file=sys.stderr)
return 'Successfully built project'
else:
print(f'ERROR: Build failed: {result.stderr}', file=sys.stderr)
return f'Build failed: {result.stderr}'
except Exception as e:
print(f'ERROR: Build failed: {str(e)}', file=sys.stderr)
return f'Build failed: {str(e)}'
@mcp.tool()
@@ -77,17 +133,19 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
cmd = [
sys.executable,
os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py'),
'-C',
current_project,
'set-target',
target,
]
result = subprocess.run(cmd, capture_output=True, text=True, cwd=current_project)
print(f'INFO: Setting target with command: {" ".join(cmd)} in path: {project_path}', file=sys.stderr)
result = subprocess.run(cmd, capture_output=True, text=True, cwd=project_path)
if result.returncode == 0:
print(f'INFO: Target set to: {target}', file=sys.stderr)
return f'Target set to: {target}'
else:
print(f'ERROR: Failed to set target: {result.stderr}', file=sys.stderr)
return f'Failed to set target: {result.stderr}'
except Exception as e:
print(f'ERROR: Failed to set target: {str(e)}', file=sys.stderr)
return f'Error setting target: {str(e)}'
@mcp.tool()
@@ -102,40 +160,20 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
cmd = [
sys.executable,
os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py'),
'-C',
current_project,
] + flash_args
result = subprocess.run(cmd, capture_output=True, text=True, cwd=current_project)
print(f'INFO: Flashing project with command: {" ".join(cmd)} in path: {project_path}', file=sys.stderr)
result = subprocess.run(cmd, capture_output=True, text=True, cwd=project_path)
if result.returncode == 0:
print('INFO: Flash successful', file=sys.stderr)
return f'Successfully flashed project{" to port " + port if port else ""}'
else:
print(f'ERROR: Flash failed: {result.stderr}', file=sys.stderr)
return f'Flash failed: {result.stderr}'
except Exception as e:
print(f'ERROR: Flash failed: {str(e)}', file=sys.stderr)
return f'Error flashing: {str(e)}'
@mcp.tool()
def monitor_serial(port: str | None = None) -> str:
"""Start serial monitor (returns immediately, monitor runs in background)"""
try:
monitor_args = []
if port:
monitor_args.extend(['-p', port])
monitor_args.append('monitor')
cmd = [
sys.executable,
os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py'),
'-C',
current_project,
] + monitor_args
# Start monitor in background
subprocess.Popen(cmd, cwd=current_project)
return f'Serial monitor started{" on port " + port if port else ""}'
except Exception as e:
return f'Error starting monitor: {str(e)}'
@mcp.tool()
def clean_project() -> str:
"""Clean build artifacts"""
@@ -143,90 +181,64 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
cmd = [
sys.executable,
os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py'),
'-C',
current_project,
'clean',
]
result = subprocess.run(cmd, capture_output=True, text=True, cwd=current_project)
print(f'INFO: Cleaning project with command: {" ".join(cmd)} in path: {project_path}', file=sys.stderr)
result = subprocess.run(cmd, capture_output=True, text=True, cwd=project_path)
if result.returncode == 0:
print('INFO: Project cleaned successfully', file=sys.stderr)
return 'Project cleaned successfully'
else:
print(f'ERROR: Clean failed: {result.stderr}', file=sys.stderr)
return f'Clean failed: {result.stderr}'
except Exception as e:
print(f'ERROR: Error cleaning: {str(e)}', file=sys.stderr)
return f'Error cleaning: {str(e)}'
@mcp.tool()
def menuconfig() -> str:
"""Open menuconfig interface (terminal-based)"""
try:
cmd = [
sys.executable,
os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py'),
'-C',
current_project,
'menuconfig',
]
subprocess.run(cmd, cwd=current_project)
return 'Menuconfig completed'
except Exception as e:
return f'Error opening menuconfig: {str(e)}'
# === RESOURCES (Data Access) ===
@mcp.resource('project://config')
def get_project_config() -> str:
"""Get current project configuration"""
build_dir = args.get('build_dir', '')
config: dict[str, Any] = {}
if not os.path.exists(build_dir):
config['build_dir_exists'] = False
return json.dumps(config, indent=2)
config['build_dir'] = build_dir
proj_desc_fn = f'{build_dir}/project_description.json'
config['project_description'] = 'Project description does not exist'
try:
config_data = {}
with open(proj_desc_fn, encoding='utf-8') as f:
config['project_description'] = json.load(f)
except (OSError, ValueError):
pass
# Get target
config_data['target'] = get_target(current_project)
# Get build directory info
build_dir = os.path.join(current_project, 'build')
config_data['build_dir'] = build_dir
config_data['build_exists'] = os.path.exists(build_dir)
# Get project description if available
try:
ctx = get_build_context()
if 'proj_desc' in ctx:
config_data['project_description'] = ctx['proj_desc']
except Exception:
pass
# Get sdkconfig info
sdkconfig_path = os.path.join(current_project, 'sdkconfig')
config_data['sdkconfig_exists'] = os.path.exists(sdkconfig_path)
return json.dumps(config_data, indent=2)
except Exception as e:
return f'Error getting config: {str(e)}'
return json.dumps(config, indent=2)
@mcp.resource('project://status')
def get_project_status() -> str:
"""Get current project build status"""
try:
status = {
'project_path': current_project,
'target': get_target(current_project),
'project_path': project_path,
'target': get_target(project_path),
'idf_version': idf_version(),
'build_dir': os.path.join(current_project, 'build'),
}
# Check if built
build_dir = os.path.join(current_project, 'build')
build_dir = args.build_dir
if os.path.exists(build_dir):
status['built'] = True
# Check for common build artifacts
status['build_dir'] = build_dir
artifacts = ['bootloader', 'partition_table', 'app-flash', 'flash_args']
status['artifacts'] = {}
for artifact in artifacts:
artifact_path = os.path.join(build_dir, artifact)
status['artifacts'][artifact] = os.path.exists(artifact_path)
else:
status['built'] = False
status['build_dir_exists'] = False
return json.dumps(status, indent=2)
except Exception as e:
@@ -234,25 +246,19 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
@mcp.resource('project://devices')
def get_connected_devices() -> str:
"""Get list of connected ESP devices"""
"""Get list of connected devices"""
try:
# Use esptool to list ports
cmd = [sys.executable, '-m', 'esptool', '--list-ports']
result = subprocess.run(cmd, capture_output=True, text=True)
devices = {
'available_ports': result.stdout.strip().split('\n') if result.stdout else [],
'esptool_available': result.returncode == 0,
}
import serial.tools.list_ports
devices_on_ports = [p.device.strip() for p in serial.tools.list_ports.comports()]
print(f'Devices: {devices_on_ports}', file=sys.stderr)
devices = {'available_ports': devices_on_ports if devices_on_ports else []}
return json.dumps(devices, indent=2)
except Exception as e:
return f'Error getting devices: {str(e)}'
# Start the MCP server
print('MCP Server running on stdio...')
print('Available tools: build_project, set_target, flash_project, monitor_serial, clean_project, menuconfig')
print('Available resources: project://config, project://status, project://devices')
try:
mcp.run()
@@ -268,6 +274,6 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
'callback': start_mcp_server,
'help': 'Start MCP (Model Context Protocol) server for AI integration',
'options': [],
}
},
}
}