esp-workspace-mcp 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- esp_workspace_mcp-0.5.0/.env.example +35 -0
- esp_workspace_mcp-0.5.0/MANIFEST.in +5 -0
- esp_workspace_mcp-0.5.0/PKG-INFO +190 -0
- esp_workspace_mcp-0.5.0/README.md +159 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/__init__.py +2 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/__main__.py +3 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/auth.py +74 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/cli.py +237 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/config.py +57 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/resources/__init__.py +1 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/resources/build.py +1 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/resources/git_status.py +1 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/resources/project.py +1 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/server.py +753 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/__init__.py +30 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/diagnostics.py +244 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/esp_idf.py +530 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/filesystem.py +212 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/git_tools.py +366 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/phase4_debug.py +163 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/phase4_symbols.py +266 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/phase4_tools.py +150 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/phase4_uart.py +228 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/search.py +216 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/serial_tools.py +309 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/session_tools.py +108 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/tools/shell.py +120 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/utils/__init__.py +1 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/utils/process.py +232 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp/utils/security.py +101 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp.egg-info/PKG-INFO +190 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp.egg-info/SOURCES.txt +37 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp.egg-info/dependency_links.txt +1 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp.egg-info/entry_points.txt +4 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp.egg-info/requires.txt +12 -0
- esp_workspace_mcp-0.5.0/esp_workspace_mcp.egg-info/top_level.txt +1 -0
- esp_workspace_mcp-0.5.0/pyproject.toml +53 -0
- esp_workspace_mcp-0.5.0/requirements.txt +7 -0
- esp_workspace_mcp-0.5.0/setup.cfg +4 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ESP-Workspace MCP Server Configuration
|
|
2
|
+
# Copy to .env and fill in your values
|
|
3
|
+
|
|
4
|
+
# Authentication (required)
|
|
5
|
+
# Generate a random token: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
6
|
+
MCP_API_TOKEN=
|
|
7
|
+
|
|
8
|
+
# Network
|
|
9
|
+
MCP_HOST=0.0.0.0
|
|
10
|
+
MCP_PORT=8765
|
|
11
|
+
|
|
12
|
+
# Logging
|
|
13
|
+
MCP_LOG_LEVEL=INFO
|
|
14
|
+
|
|
15
|
+
# Filesystem sandbox — comma-separated allowed roots
|
|
16
|
+
MCP_ALLOWED_ROOTS=/home/juergen/AIRcableLLC
|
|
17
|
+
|
|
18
|
+
# ESP-IDF / EIM configuration
|
|
19
|
+
# Default WISH_PRODUCT value (e.g. TargetS3, TargetC6)
|
|
20
|
+
# Can be overridden per tool call
|
|
21
|
+
MCP_WISH_PRODUCT=TargetS3
|
|
22
|
+
|
|
23
|
+
# Path to ESP-IDF installation (optional, eim handles env)
|
|
24
|
+
MCP_IDF_PATH=
|
|
25
|
+
|
|
26
|
+
# Path to eim executable
|
|
27
|
+
MCP_EIM_PATH=eim
|
|
28
|
+
|
|
29
|
+
# Timeouts and limits
|
|
30
|
+
MCP_DEFAULT_TIMEOUT=30
|
|
31
|
+
MCP_MAX_TIMEOUT=300
|
|
32
|
+
MCP_OUTPUT_LIMIT=51200
|
|
33
|
+
|
|
34
|
+
# Job TTL in seconds
|
|
35
|
+
MCP_JOB_TTL_SECONDS=3600
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: esp-workspace-mcp
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Full autonomous embedded firmware workspace MCP server for ESP-IDF
|
|
5
|
+
Author: AIRcable LLC
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/aircable/ESP-Workspace-MCP-Server-for-Hermes
|
|
8
|
+
Project-URL: Repository, https://github.com/aircable/ESP-Workspace-MCP-Server-for-Hermes
|
|
9
|
+
Project-URL: Issues, https://github.com/aircable/ESP-Workspace-MCP-Server-for-Hermes/issues
|
|
10
|
+
Keywords: mcp,esp-idf,esp32,firmware,embedded,ai-agent,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: mcp>=1.0
|
|
21
|
+
Requires-Dist: fastapi>=0.115
|
|
22
|
+
Requires-Dist: uvicorn>=0.34
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
25
|
+
Requires-Dist: gitpython>=3.1
|
|
26
|
+
Requires-Dist: pyserial>=3.5
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
31
|
+
|
|
32
|
+
# ESP-Workspace MCP
|
|
33
|
+
|
|
34
|
+
A full autonomous embedded firmware workspace server — the MCP (Model Context Protocol) infrastructure layer for AI agents working with ESP-IDF projects.
|
|
35
|
+
|
|
36
|
+
## What it does
|
|
37
|
+
|
|
38
|
+
ESP-Workspace MCP turns an AI coding assistant into an autonomous firmware engineer. It provides tools for:
|
|
39
|
+
|
|
40
|
+
- **Filesystem operations** — read, write, list, glob with path sandboxing
|
|
41
|
+
- **Shell execution** — run commands with timeout, background jobs, output capture
|
|
42
|
+
- **ESP-IDF build system** — build, flash, clean, reconfigure via `eim run` with `WISH_PRODUCT` support
|
|
43
|
+
- **Security** — Bearer token auth, path traversal prevention, configurable filesystem roots
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### Prerequisites
|
|
48
|
+
|
|
49
|
+
- Python 3.10+
|
|
50
|
+
- [Espressif Install Manager (`eim`](https://github.com/espressif/esp-idf-installer) — handles ESP-IDF installation and environment setup
|
|
51
|
+
|
|
52
|
+
### Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install esp-workspace-mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or from source:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/AIRcableLLC/esp-workspace-mcp.git
|
|
62
|
+
cd esp-workspace-mcp
|
|
63
|
+
python -m venv .venv
|
|
64
|
+
source .venv/bin/activate
|
|
65
|
+
pip install -e ".[dev]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Configuration
|
|
69
|
+
|
|
70
|
+
Create a `.env` file:
|
|
71
|
+
|
|
72
|
+
```env
|
|
73
|
+
MCP_API_TOKEN=your-secret-token-here
|
|
74
|
+
MCP_HOST=0.0.0.0
|
|
75
|
+
MCP_PORT=8765
|
|
76
|
+
MCP_ALLOWED_ROOTS=/home/user/projects
|
|
77
|
+
MCP_WISH_PRODUCT=TargetS3
|
|
78
|
+
MCP_EIM_PATH=eim
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or set environment variables directly.
|
|
82
|
+
|
|
83
|
+
### Run the server
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# SSE mode (default, for network access)
|
|
87
|
+
python run_server.py
|
|
88
|
+
|
|
89
|
+
# Stdio mode (for local MCP clients)
|
|
90
|
+
python run_server.py --stdio
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The server exposes:
|
|
94
|
+
- `GET /sse` — SSE connection endpoint (requires Bearer token)
|
|
95
|
+
- `POST /messages` — MCP message endpoint
|
|
96
|
+
- `GET /health` — health check (no auth required)
|
|
97
|
+
|
|
98
|
+
## MCP Tools
|
|
99
|
+
|
|
100
|
+
### Filesystem Tools
|
|
101
|
+
|
|
102
|
+
| Tool | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `read_file(path, offset, limit)` | Read text file with pagination |
|
|
105
|
+
| `write_file(path, content)` | Write content to a file |
|
|
106
|
+
| `append_file(path, content)` | Append to an existing file |
|
|
107
|
+
| `list_dir(path)` | List directory entries |
|
|
108
|
+
| `create_dir(path)` | Create directory recursively |
|
|
109
|
+
| `delete_path(path)` | Delete file or empty directory |
|
|
110
|
+
| `file_stat(path)` | Get file/directory metadata |
|
|
111
|
+
| `glob_search(pattern, path)` | Find files by glob pattern |
|
|
112
|
+
|
|
113
|
+
### Shell Tools
|
|
114
|
+
|
|
115
|
+
| Tool | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `run_command(cmd, cwd, timeout)` | Execute command and wait |
|
|
118
|
+
| `start_process(cmd, cwd)` | Start background job |
|
|
119
|
+
| `get_job_output(job_id, offset)` | Read job output |
|
|
120
|
+
| `kill_job(job_id)` | Terminate background job |
|
|
121
|
+
| `list_jobs()` | List all jobs |
|
|
122
|
+
|
|
123
|
+
### ESP-IDF Tools
|
|
124
|
+
|
|
125
|
+
All ESP-IDF operations are invoked through `eim run "WISH_PRODUCT=<product> idf.py ..."` which handles environment setup automatically.
|
|
126
|
+
|
|
127
|
+
| Tool | Description |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `eim_run(commands, project_dir, wish_product)` | Run any `idf.py` command via `eim` |
|
|
130
|
+
| `build_project(project_dir, wish_product, reconfigure)` | Build an ESP-IDF project |
|
|
131
|
+
| `set_target(project_dir, target, wish_product)` | Set target chip (esp32, esp32s3, etc.) |
|
|
132
|
+
| `flash_project(project_dir, port, wish_product)` | Flash firmware to device |
|
|
133
|
+
| `clean_project(project_dir)` | Clean build artifacts |
|
|
134
|
+
| `fullclean_project(project_dir)` | Remove entire build directory |
|
|
135
|
+
| `reconfigure_project(project_dir, wish_product)` | Regenerate sdkconfig + CMake cache |
|
|
136
|
+
|
|
137
|
+
### Build Step Decision Tree
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Is sdkconfig missing or have defaults changed?
|
|
141
|
+
├── YES → eim_run("reconfigure build", project_dir, wish_product)
|
|
142
|
+
└── NO → eim_run("build", project_dir, wish_product)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Security
|
|
146
|
+
|
|
147
|
+
| Control | Implementation |
|
|
148
|
+
|---|---|
|
|
149
|
+
| Filesystem sandbox | All paths validated against `MCP_ALLOWED_ROOTS` |
|
|
150
|
+
| Path traversal prevention | Symlink-resolved, `..` escapes rejected |
|
|
151
|
+
| Shell timeout | Configurable, default 30s, max 300s |
|
|
152
|
+
| Output truncation | 50 KB max per response |
|
|
153
|
+
| Authentication | Bearer token on every request |
|
|
154
|
+
| Credential safety | Tokens in env vars only, never logged |
|
|
155
|
+
|
|
156
|
+
## Integration with AI Agents
|
|
157
|
+
|
|
158
|
+
### Hermes
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
hermes mcp add esp-workspace \
|
|
162
|
+
--url http://host:port/sse \
|
|
163
|
+
--header "Authorization: Bearer your-token"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Generic MCP Client
|
|
167
|
+
|
|
168
|
+
Any MCP-compatible client can connect to the SSE endpoint. The server implements the standard MCP protocol over SSE/HTTP transport.
|
|
169
|
+
|
|
170
|
+
## Architecture
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
esp_workspace_mcp/
|
|
174
|
+
├── config.py # Settings from env, fail-fast validation
|
|
175
|
+
├── server.py # FastMCP server, tool registration
|
|
176
|
+
├── auth.py # Bearer token middleware
|
|
177
|
+
├── tools/
|
|
178
|
+
│ ├── filesystem.py # 8 path-validated file operations
|
|
179
|
+
│ ├── shell.py # Command execution + background jobs
|
|
180
|
+
│ └── esp_idf.py # eim-run wrapper, build/flash/clean
|
|
181
|
+
├── utils/
|
|
182
|
+
│ ├── security.py # Path sandboxing, symlink resolution
|
|
183
|
+
│ └── process.py # Subprocess management, JobManager
|
|
184
|
+
└── resources/
|
|
185
|
+
└── project.py # MCP resources (project status, etc.)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
Apache-2.0
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# ESP-Workspace MCP
|
|
2
|
+
|
|
3
|
+
A full autonomous embedded firmware workspace server — the MCP (Model Context Protocol) infrastructure layer for AI agents working with ESP-IDF projects.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
ESP-Workspace MCP turns an AI coding assistant into an autonomous firmware engineer. It provides tools for:
|
|
8
|
+
|
|
9
|
+
- **Filesystem operations** — read, write, list, glob with path sandboxing
|
|
10
|
+
- **Shell execution** — run commands with timeout, background jobs, output capture
|
|
11
|
+
- **ESP-IDF build system** — build, flash, clean, reconfigure via `eim run` with `WISH_PRODUCT` support
|
|
12
|
+
- **Security** — Bearer token auth, path traversal prevention, configurable filesystem roots
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Prerequisites
|
|
17
|
+
|
|
18
|
+
- Python 3.10+
|
|
19
|
+
- [Espressif Install Manager (`eim`](https://github.com/espressif/esp-idf-installer) — handles ESP-IDF installation and environment setup
|
|
20
|
+
|
|
21
|
+
### Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install esp-workspace-mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or from source:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/AIRcableLLC/esp-workspace-mcp.git
|
|
31
|
+
cd esp-workspace-mcp
|
|
32
|
+
python -m venv .venv
|
|
33
|
+
source .venv/bin/activate
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Configuration
|
|
38
|
+
|
|
39
|
+
Create a `.env` file:
|
|
40
|
+
|
|
41
|
+
```env
|
|
42
|
+
MCP_API_TOKEN=your-secret-token-here
|
|
43
|
+
MCP_HOST=0.0.0.0
|
|
44
|
+
MCP_PORT=8765
|
|
45
|
+
MCP_ALLOWED_ROOTS=/home/user/projects
|
|
46
|
+
MCP_WISH_PRODUCT=TargetS3
|
|
47
|
+
MCP_EIM_PATH=eim
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or set environment variables directly.
|
|
51
|
+
|
|
52
|
+
### Run the server
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# SSE mode (default, for network access)
|
|
56
|
+
python run_server.py
|
|
57
|
+
|
|
58
|
+
# Stdio mode (for local MCP clients)
|
|
59
|
+
python run_server.py --stdio
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The server exposes:
|
|
63
|
+
- `GET /sse` — SSE connection endpoint (requires Bearer token)
|
|
64
|
+
- `POST /messages` — MCP message endpoint
|
|
65
|
+
- `GET /health` — health check (no auth required)
|
|
66
|
+
|
|
67
|
+
## MCP Tools
|
|
68
|
+
|
|
69
|
+
### Filesystem Tools
|
|
70
|
+
|
|
71
|
+
| Tool | Description |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `read_file(path, offset, limit)` | Read text file with pagination |
|
|
74
|
+
| `write_file(path, content)` | Write content to a file |
|
|
75
|
+
| `append_file(path, content)` | Append to an existing file |
|
|
76
|
+
| `list_dir(path)` | List directory entries |
|
|
77
|
+
| `create_dir(path)` | Create directory recursively |
|
|
78
|
+
| `delete_path(path)` | Delete file or empty directory |
|
|
79
|
+
| `file_stat(path)` | Get file/directory metadata |
|
|
80
|
+
| `glob_search(pattern, path)` | Find files by glob pattern |
|
|
81
|
+
|
|
82
|
+
### Shell Tools
|
|
83
|
+
|
|
84
|
+
| Tool | Description |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `run_command(cmd, cwd, timeout)` | Execute command and wait |
|
|
87
|
+
| `start_process(cmd, cwd)` | Start background job |
|
|
88
|
+
| `get_job_output(job_id, offset)` | Read job output |
|
|
89
|
+
| `kill_job(job_id)` | Terminate background job |
|
|
90
|
+
| `list_jobs()` | List all jobs |
|
|
91
|
+
|
|
92
|
+
### ESP-IDF Tools
|
|
93
|
+
|
|
94
|
+
All ESP-IDF operations are invoked through `eim run "WISH_PRODUCT=<product> idf.py ..."` which handles environment setup automatically.
|
|
95
|
+
|
|
96
|
+
| Tool | Description |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `eim_run(commands, project_dir, wish_product)` | Run any `idf.py` command via `eim` |
|
|
99
|
+
| `build_project(project_dir, wish_product, reconfigure)` | Build an ESP-IDF project |
|
|
100
|
+
| `set_target(project_dir, target, wish_product)` | Set target chip (esp32, esp32s3, etc.) |
|
|
101
|
+
| `flash_project(project_dir, port, wish_product)` | Flash firmware to device |
|
|
102
|
+
| `clean_project(project_dir)` | Clean build artifacts |
|
|
103
|
+
| `fullclean_project(project_dir)` | Remove entire build directory |
|
|
104
|
+
| `reconfigure_project(project_dir, wish_product)` | Regenerate sdkconfig + CMake cache |
|
|
105
|
+
|
|
106
|
+
### Build Step Decision Tree
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Is sdkconfig missing or have defaults changed?
|
|
110
|
+
├── YES → eim_run("reconfigure build", project_dir, wish_product)
|
|
111
|
+
└── NO → eim_run("build", project_dir, wish_product)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Security
|
|
115
|
+
|
|
116
|
+
| Control | Implementation |
|
|
117
|
+
|---|---|
|
|
118
|
+
| Filesystem sandbox | All paths validated against `MCP_ALLOWED_ROOTS` |
|
|
119
|
+
| Path traversal prevention | Symlink-resolved, `..` escapes rejected |
|
|
120
|
+
| Shell timeout | Configurable, default 30s, max 300s |
|
|
121
|
+
| Output truncation | 50 KB max per response |
|
|
122
|
+
| Authentication | Bearer token on every request |
|
|
123
|
+
| Credential safety | Tokens in env vars only, never logged |
|
|
124
|
+
|
|
125
|
+
## Integration with AI Agents
|
|
126
|
+
|
|
127
|
+
### Hermes
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
hermes mcp add esp-workspace \
|
|
131
|
+
--url http://host:port/sse \
|
|
132
|
+
--header "Authorization: Bearer your-token"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Generic MCP Client
|
|
136
|
+
|
|
137
|
+
Any MCP-compatible client can connect to the SSE endpoint. The server implements the standard MCP protocol over SSE/HTTP transport.
|
|
138
|
+
|
|
139
|
+
## Architecture
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
esp_workspace_mcp/
|
|
143
|
+
├── config.py # Settings from env, fail-fast validation
|
|
144
|
+
├── server.py # FastMCP server, tool registration
|
|
145
|
+
├── auth.py # Bearer token middleware
|
|
146
|
+
├── tools/
|
|
147
|
+
│ ├── filesystem.py # 8 path-validated file operations
|
|
148
|
+
│ ├── shell.py # Command execution + background jobs
|
|
149
|
+
│ └── esp_idf.py # eim-run wrapper, build/flash/clean
|
|
150
|
+
├── utils/
|
|
151
|
+
│ ├── security.py # Path sandboxing, symlink resolution
|
|
152
|
+
│ └── process.py # Subprocess management, JobManager
|
|
153
|
+
└── resources/
|
|
154
|
+
└── project.py # MCP resources (project status, etc.)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
Apache-2.0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""SSE transport with Bearer token authentication."""
|
|
2
|
+
import logging
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import Request, Response
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _mask_token(token: str, show: int = 4) -> str:
|
|
14
|
+
if not token:
|
|
15
|
+
return ""
|
|
16
|
+
if len(token) <= show:
|
|
17
|
+
return "*" * len(token)
|
|
18
|
+
return f"***{token[-show:]}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BearerAuthMiddleware(BaseHTTPMiddleware):
|
|
22
|
+
"""Middleware that enforces Bearer token authentication."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, app, api_token: str, exempt_paths: Optional[list] = None):
|
|
25
|
+
super().__init__(app)
|
|
26
|
+
self.api_token = api_token
|
|
27
|
+
self.exempt_paths = exempt_paths or ["/health"]
|
|
28
|
+
|
|
29
|
+
async def dispatch(self, request: Request, call_next):
|
|
30
|
+
# Allow health check without auth
|
|
31
|
+
if request.url.path in self.exempt_paths:
|
|
32
|
+
logger.debug("Auth: exempt path %s", request.url.path)
|
|
33
|
+
return await call_next(request)
|
|
34
|
+
|
|
35
|
+
# Check Authorization header
|
|
36
|
+
auth = request.headers.get("Authorization", "")
|
|
37
|
+
provided_token = None
|
|
38
|
+
if auth.startswith("Bearer "):
|
|
39
|
+
provided_token = auth[7:]
|
|
40
|
+
|
|
41
|
+
# Debug logging (masked tokens only)
|
|
42
|
+
logger.debug(
|
|
43
|
+
"Auth check: path=%s method=%s auth_header=%s provided=%s expected=%s",
|
|
44
|
+
request.url.path,
|
|
45
|
+
request.method,
|
|
46
|
+
auth[:7] + "..." if auth else "",
|
|
47
|
+
_mask_token(provided_token),
|
|
48
|
+
_mask_token(self.api_token),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if not provided_token or provided_token != self.api_token:
|
|
52
|
+
logger.warning(
|
|
53
|
+
"Unauthorized access attempt on %s: provided=%s expected=%s",
|
|
54
|
+
request.url.path,
|
|
55
|
+
_mask_token(provided_token),
|
|
56
|
+
_mask_token(self.api_token),
|
|
57
|
+
)
|
|
58
|
+
return JSONResponse(
|
|
59
|
+
status_code=401,
|
|
60
|
+
content={"error": "Unauthorized: invalid or missing Bearer token"},
|
|
61
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return await call_next(request)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def require_token(api_token: str):
|
|
68
|
+
"""Decorator for requiring Bearer token on a specific endpoint."""
|
|
69
|
+
def decorator(func):
|
|
70
|
+
@wraps(func)
|
|
71
|
+
async def wrapper(*args, **kwargs):
|
|
72
|
+
return await func(*args, **kwargs)
|
|
73
|
+
return wrapper
|
|
74
|
+
return decorator
|