feat(tools): mcp-server added create-project tool

This commit is contained in:
Marek Fiala
2026-06-25 09:13:49 +02:00
parent 40fe010be4
commit 66bbd208d3
2 changed files with 101 additions and 0 deletions
+42
View File
@@ -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.
+59
View File
@@ -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