mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
feat(tools): mcp-server added create-project tool
This commit is contained in:
@@ -258,6 +258,48 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
||||
print(f'ERROR: Flash failed: {str(e)}', file=sys.stderr)
|
||||
return f'Error flashing: {str(e)}'
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
'Create a new ESP-IDF project from the sample template (runs `idf.py create-project`). '
|
||||
'A directory named <name> is created inside <path>. Use this only when the user asks '
|
||||
'to bootstrap a new project; do not use it on an existing project.'
|
||||
)
|
||||
)
|
||||
def create_project(name: str, path: str | None = None) -> str:
|
||||
"""Create a new ESP-IDF project from the sample template.
|
||||
|
||||
Args:
|
||||
name: Name of the new project; also becomes the subdirectory name.
|
||||
path: Optional absolute path to the parent directory in which the
|
||||
<name>/ subdirectory will be created. Leave as None to create
|
||||
it in the directory the MCP server was launched from.
|
||||
"""
|
||||
parent_dir = path or project_path or os.getcwd()
|
||||
if not os.path.isdir(parent_dir):
|
||||
return f'Parent directory does not exist: {parent_dir}'
|
||||
try:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py'),
|
||||
'-C',
|
||||
parent_dir,
|
||||
'create-project',
|
||||
name,
|
||||
]
|
||||
print(f'INFO: Creating project "{name}" in {parent_dir}', file=sys.stderr)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
project_path_new = os.path.join(parent_dir, name)
|
||||
print(f'INFO: Project "{name}" created at {project_path_new}', file=sys.stderr)
|
||||
return f'Project "{name}" created at {project_path_new}'
|
||||
else:
|
||||
output = result.stderr or result.stdout
|
||||
print(f'ERROR: Failed to create project: {output}', file=sys.stderr)
|
||||
return f'Failed to create project "{name}": {output}'
|
||||
except Exception as e:
|
||||
print(f'ERROR: Failed to create project: {str(e)}', file=sys.stderr)
|
||||
return f'Failed to create project "{name}": {str(e)}'
|
||||
|
||||
@mcp.tool(description=f'Remove build artifacts from the ESP-IDF project (runs `idf.py clean`). {bound_hint}')
|
||||
def clean_project(project_dir: str | None = None) -> str:
|
||||
"""Remove build artifacts from the ESP-IDF project.
|
||||
|
||||
@@ -365,6 +365,65 @@ class TestFlashProject:
|
||||
assert cmd[cmd.index('-C') + 1] == str(proj)
|
||||
|
||||
|
||||
class TestCreateProject:
|
||||
def test_creates_project_with_explicit_path(
|
||||
self, tmp_path: Path, mcp_ext: tuple[types.ModuleType, _MockFastMCP], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
mod, mock_mcp = mcp_ext
|
||||
monkeypatch.setenv('IDF_PATH', str(tmp_path))
|
||||
# Start from a non-project directory — that is the whole point of this tool
|
||||
tools, _ = _start_server(mcp_ext, mock_mcp, str(tmp_path))
|
||||
|
||||
with mock.patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = mock.Mock(returncode=0, stdout='', stderr='')
|
||||
result = tools['create_project']('my_app', path=str(tmp_path))
|
||||
|
||||
assert 'my_app' in result
|
||||
assert str(tmp_path) in result
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert 'create-project' in cmd
|
||||
assert 'my_app' in cmd
|
||||
assert '-C' in cmd
|
||||
assert cmd[cmd.index('-C') + 1] == str(tmp_path)
|
||||
|
||||
def test_uses_project_path_when_no_path_given(
|
||||
self, tmp_path: Path, mcp_ext: tuple[types.ModuleType, _MockFastMCP], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
mod, mock_mcp = mcp_ext
|
||||
monkeypatch.setenv('IDF_PATH', str(tmp_path))
|
||||
tools, _ = _start_server(mcp_ext, mock_mcp, str(tmp_path))
|
||||
|
||||
with mock.patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = mock.Mock(returncode=0, stdout='', stderr='')
|
||||
tools['create_project']('my_app')
|
||||
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert cmd[cmd.index('-C') + 1] == str(tmp_path)
|
||||
|
||||
def test_returns_error_when_parent_dir_missing(
|
||||
self, tmp_path: Path, mcp_ext: tuple[types.ModuleType, _MockFastMCP], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
mod, mock_mcp = mcp_ext
|
||||
tools, _ = _start_server(mcp_ext, mock_mcp, str(tmp_path))
|
||||
|
||||
result = tools['create_project']('my_app', path=str(tmp_path / 'nonexistent'))
|
||||
assert 'does not exist' in result
|
||||
|
||||
def test_returns_error_on_idf_failure(
|
||||
self, tmp_path: Path, mcp_ext: tuple[types.ModuleType, _MockFastMCP], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
mod, mock_mcp = mcp_ext
|
||||
monkeypatch.setenv('IDF_PATH', str(tmp_path))
|
||||
tools, _ = _start_server(mcp_ext, mock_mcp, str(tmp_path))
|
||||
|
||||
with mock.patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = mock.Mock(returncode=3, stdout='', stderr='directory not empty')
|
||||
result = tools['create_project']('my_app', path=str(tmp_path))
|
||||
|
||||
assert 'Failed to create project' in result
|
||||
assert 'directory not empty' in result
|
||||
|
||||
|
||||
class TestCleanProject:
|
||||
def test_returns_error_when_no_valid_dir(
|
||||
self, tmp_path: Path, mcp_ext: tuple[types.ModuleType, _MockFastMCP], monkeypatch: pytest.MonkeyPatch
|
||||
|
||||
Reference in New Issue
Block a user