mcp-yieldshell 0.2.0__tar.gz → 0.4.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.
- mcp_yieldshell-0.4.0/PKG-INFO +302 -0
- mcp_yieldshell-0.4.0/README.md +294 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/pyproject.toml +1 -1
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/config.py +37 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/process/manager.py +130 -3
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/server.py +47 -1
- mcp_yieldshell-0.4.0/src/mcp_yieldshell/types.py +100 -0
- mcp_yieldshell-0.4.0/tests/test_config.py +379 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/tests/test_integration.py +47 -44
- mcp_yieldshell-0.4.0/tests/test_release.py +300 -0
- mcp_yieldshell-0.4.0/tests/test_side_effects.py +880 -0
- mcp_yieldshell-0.2.0/PKG-INFO +0 -277
- mcp_yieldshell-0.2.0/README.md +0 -269
- mcp_yieldshell-0.2.0/src/mcp_yieldshell/types.py +0 -32
- mcp_yieldshell-0.2.0/tests/test_config.py +0 -92
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/.gitignore +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/__init__.py +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/__main__.py +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/process/__init__.py +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/process/ring_buffer.py +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/process/spawn.py +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/src/mcp_yieldshell/security.py +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/tests/__init__.py +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/tests/test_ring_buffer.py +0 -0
- {mcp_yieldshell-0.2.0 → mcp_yieldshell-0.4.0}/tests/test_security.py +0 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-yieldshell
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A drop-in shell MCP that auto-yields long-running commands into managed background processes.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: mcp<2,>=1.9.0
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# YieldShell MCP
|
|
10
|
+
|
|
11
|
+
A drop-in shell MCP server that auto-yields long-running commands into managed background processes.
|
|
12
|
+
|
|
13
|
+
## Why Auto-Yielding?
|
|
14
|
+
|
|
15
|
+
Most shell tools present a frustrating choice: either block the LLM agent until the command finishes, or force the agent to decide upfront that a command should run in the background.
|
|
16
|
+
|
|
17
|
+
**YieldShell MCP** solves this by keeping normal foreground semantics for fast commands, then automatically promoting long-running commands into managed background processes after a brief delay (`yield_ms`, default: 5 seconds).
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
graph TD
|
|
21
|
+
A[exec_command] --> B["Wait for yield_ms (default: 5s)"]
|
|
22
|
+
B --> C{Is process still running?}
|
|
23
|
+
C -->|Yes| D["backgrounded<br>Returns process_id"]
|
|
24
|
+
C -->|No| E["completed<br>Returns full output"]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **Fast Commands** (e.g., `echo hello`, `ls`): Complete instantly, returning the output immediately.
|
|
28
|
+
- **Long-Running Commands** (e.g., `npm run dev`, `docker build`, `sleep 60`): Automatically yield control back to the agent with a `process_id` and a snapshot of initial output, letting the agent decide when to `read`, `wait`, or `stop` the process.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
### From Registry (Recommended)
|
|
35
|
+
|
|
36
|
+
To run the published package via `uv`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv tool install mcp-yieldshell
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Local Development
|
|
43
|
+
|
|
44
|
+
To clone and run locally:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone <repo-url> && cd mcp-yieldshell
|
|
48
|
+
uv sync
|
|
49
|
+
uv run mcp-yieldshell
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## MCP Client Configuration
|
|
55
|
+
|
|
56
|
+
### Claude Desktop
|
|
57
|
+
|
|
58
|
+
To configure the server in Claude Desktop, add the configuration below to your Claude Desktop config file:
|
|
59
|
+
|
|
60
|
+
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
61
|
+
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
62
|
+
|
|
63
|
+
#### Production (via uvx)
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"yieldshell": {
|
|
69
|
+
"command": "uvx",
|
|
70
|
+
"args": ["mcp-yieldshell"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Production with Security Restrictions
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"yieldshell": {
|
|
82
|
+
"command": "uvx",
|
|
83
|
+
"args": ["mcp-yieldshell"],
|
|
84
|
+
"env": {
|
|
85
|
+
"YIELDSHELL_ALLOWED_CWDS": "/home/user/projects:/tmp/build",
|
|
86
|
+
"YIELDSHELL_DEFAULT_TIMEOUT_MS": "300000"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Local Development Setup
|
|
94
|
+
|
|
95
|
+
Replace `/path/to/mcp-yieldshell` with the absolute path to your cloned repository:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"yieldshell": {
|
|
101
|
+
"command": "uv",
|
|
102
|
+
"args": [
|
|
103
|
+
"--directory",
|
|
104
|
+
"/path/to/mcp-yieldshell",
|
|
105
|
+
"run",
|
|
106
|
+
"mcp-yieldshell"
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Cursor
|
|
114
|
+
|
|
115
|
+
To configure the server in Cursor:
|
|
116
|
+
1. Open **Cursor Settings** -> **Features** -> **MCP**.
|
|
117
|
+
2. Click **+ Add New MCP Server**.
|
|
118
|
+
3. Fill out the form:
|
|
119
|
+
- **Name**: `yieldshell`
|
|
120
|
+
- **Type**: `stdio`
|
|
121
|
+
- **Command**: `uvx mcp-yieldshell` (or `uv --directory /path/to/mcp-yieldshell run mcp-yieldshell` for local development)
|
|
122
|
+
|
|
123
|
+
### OpenCode
|
|
124
|
+
|
|
125
|
+
Add to your OpenCode MCP settings:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"yieldshell": {
|
|
131
|
+
"command": "uvx",
|
|
132
|
+
"args": ["mcp-yieldshell"]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Tool Reference
|
|
141
|
+
|
|
142
|
+
### `exec`
|
|
143
|
+
Execute a shell command. If the command runs longer than `yield_ms`, it yields a `process_id` and runs in the background.
|
|
144
|
+
|
|
145
|
+
* **Parameters**:
|
|
146
|
+
* `command` (string, **required**): The command string to execute in the shell.
|
|
147
|
+
* `side_effects` (array of string, **required**): The side-effect categories this command may plausibly have. Must contain at least one entry drawn from the enum below. Use `["NONE"]` for commands with no meaningful side effects. `NONE` is exclusive and must not be combined with any other category. The server rejects the call with `failed_to_start` if any declared category is configured as blocked.
|
|
148
|
+
* Allowed values: `CHANGES_NETWORK_CONFIGURATION`, `CHANGES_PACKAGES_OR_DEPENDENCIES`, `CONSUMES_SIGNIFICANT_RESOURCES`, `DELETES_FILES`, `EXPOSES_SECRETS`, `KILLS_AGENT_PROCESS`, `MAKES_NETWORK_REQUESTS`, `MODIFIES_OS_SETTINGS`, `MODIFIES_OS_USER_SETTINGS`, `MODIFIES_OUTSIDE_WORKSPACE`, `MODIFIES_PRODUCTION_SERVICES`, `MODIFIES_PROTECTED_FILES`, `MODIFIES_SECURITY_CONTROLS`, `MODIFIES_WORKSPACE_FILES`, `NONE`, `OTHER`, `RUNS_INLINE_CODE`, `RUNS_PRIVILEGED_COMMANDS`, `STOPS_OR_RESTARTS_SERVICES`, `UNKNOWN`, `USES_DESTRUCTIVE_GIT_OPERATION`.
|
|
149
|
+
* `RUNS_INLINE_CODE` is in the default blocklist. It covers commands that execute code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `curl ... | sh`). It does not cover simply creating a script or executable file unless the same command also executes inline code. The safer next action is to write the content to a reviewable workspace file and execute it in a small, inspectable step. Operators can unblock the category via `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=`.
|
|
150
|
+
* `cwd` (string, optional): Working directory for the command. Must be under allowed roots if `YIELDSHELL_ALLOWED_CWDS` is set. Defaults to `YIELDSHELL_DEFAULT_CWD`.
|
|
151
|
+
* `env` (object of string to string, optional): Additive environment variable overlay. Merged into the parent environment.
|
|
152
|
+
* `shell` (string, optional): Accepted but has no effect in v1. Commands always run via the platform's default shell.
|
|
153
|
+
* `stdin` (string, optional): Initial text input written to standard input immediately after spawning.
|
|
154
|
+
* `name` (string, optional): A human-readable label/name to identify this process.
|
|
155
|
+
* `yield_ms` (integer, optional): Milliseconds to wait before yielding execution to background. Clamped by `YIELDSHELL_MAX_YIELD_MS`. Defaults to `YIELDSHELL_DEFAULT_YIELD_MS` (5000ms).
|
|
156
|
+
* `timeout_ms` (integer, optional): Total execution runtime limit in milliseconds. Process is killed if it runs longer than this. Defaults to `YIELDSHELL_DEFAULT_TIMEOUT_MS` (0 = no limit).
|
|
157
|
+
* `max_output_bytes` (integer, optional): Maximum output bytes to capture in stdout/stderr ring buffers. Subject to `YIELDSHELL_MAX_OUTPUT_BYTES` cap.
|
|
158
|
+
|
|
159
|
+
* **Side-Effects Guide**:
|
|
160
|
+
* `side_effects` is required and must be a non-empty list. Declare every plausible side-effect category before running the command.
|
|
161
|
+
* `NONE` is exclusive and valid only when no meaningful side effect is expected. Use `["NONE"]` for read-only commands.
|
|
162
|
+
* The server rejects the call with `failed_to_start` if any declared category is blocked. Rejection messages name each blocked category, state that execution was stopped by policy before the process started, and provide a category-specific safer next action.
|
|
163
|
+
* Categories are case-sensitive and must use the canonical enum names listed above.
|
|
164
|
+
* **Discouraged**: executing code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `ruby -e`, `perl -e`, shell heredocs piped into interpreters, or `curl ... | sh`). Agents should prefer writing such content to a reviewable workspace file and executing it in a small, inspectable step with explicit matching `side_effects`. Declaring `RUNS_INLINE_CODE` is rejected under the default policy.
|
|
165
|
+
|
|
166
|
+
* **Side-Effect Examples**:
|
|
167
|
+
* Read-only command: `side_effects=["NONE"]`
|
|
168
|
+
* Workspace write: `side_effects=["MODIFIES_WORKSPACE_FILES"]`
|
|
169
|
+
* Dependency install: `side_effects=["CHANGES_PACKAGES_OR_DEPENDENCIES", "MAKES_NETWORK_REQUESTS"]`
|
|
170
|
+
* Network access: `side_effects=["MAKES_NETWORK_REQUESTS"]`
|
|
171
|
+
* Destructive file operations: `side_effects=["DELETES_FILES"]`
|
|
172
|
+
* Privileged command: `side_effects=["RUNS_PRIVILEGED_COMMANDS"]`
|
|
173
|
+
* Protected-file changes: `side_effects=["MODIFIES_PROTECTED_FILES"]`
|
|
174
|
+
* Inline code execution: prefer writing the content to a reviewable workspace file (for example `scripts/migrate.sql` or `tools/build.sh`) and run it in a small, inspectable step. Declaring `side_effects=["RUNS_INLINE_CODE"]` is rejected under the default policy; operators can clear that default with `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=`.
|
|
175
|
+
|
|
176
|
+
* **Output Statuses**:
|
|
177
|
+
* `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr.
|
|
178
|
+
* `backgrounded`: Process auto-yielded. Returns `process_id`, `pid`, a snapshot of initial stdout/stderr, `duration_ms`, `truncated`, and a `message` string describing that the process is running in the background.
|
|
179
|
+
* `timed_out`: Process exceeded `timeout_ms` and was terminated.
|
|
180
|
+
* `stopped`: Process was explicitly terminated.
|
|
181
|
+
* `failed_to_start`: Command could not be spawned (e.g., bad directory or policy violation).
|
|
182
|
+
* `failed`: An internal execution error occurred.
|
|
183
|
+
|
|
184
|
+
### `read`
|
|
185
|
+
Read stdout and/or stderr output from a running or completed background process.
|
|
186
|
+
|
|
187
|
+
* **Parameters**:
|
|
188
|
+
* `process_id` (string, **required**): Unique identifier of the process.
|
|
189
|
+
* `since_seq` (integer, optional): Return only output appended after this sequence number. Enables efficient incremental log polling.
|
|
190
|
+
* `max_output_bytes` (integer, optional): Clamps the response size. Defaults to the server cap.
|
|
191
|
+
* `streams` (string, default: `"both"`): The streams to read. Options: `"both"`, `"stdout"`, or `"stderr"`.
|
|
192
|
+
|
|
193
|
+
* **Returns**:
|
|
194
|
+
* `process_id`, `status`, `exit_code`, `signal`, `next_seq` (sequence index to use in subsequent `since_seq` reads), `truncated` flag. `stdout` and `stderr` text are included based on the `streams` filter — `"both"` includes both, `"stdout"` includes only `stdout`, and `"stderr"` includes only `stderr`.
|
|
195
|
+
|
|
196
|
+
### `write`
|
|
197
|
+
Write text input to the standard input (`stdin`) of a running process.
|
|
198
|
+
|
|
199
|
+
* **Parameters**:
|
|
200
|
+
* `process_id` (string, **required**): Unique identifier of the process.
|
|
201
|
+
* `input` (string, **required**): Text input to write.
|
|
202
|
+
* `newline` (boolean, default: `false`): If `true`, appends `\n` to the input.
|
|
203
|
+
|
|
204
|
+
### `wait`
|
|
205
|
+
Block execution until the process exits or the wait timeout expires. This allows the LLM to pause and await completion without spawning a new execution loop.
|
|
206
|
+
|
|
207
|
+
* **Parameters**:
|
|
208
|
+
* `process_id` (string, **required**): Unique identifier of the process.
|
|
209
|
+
* `timeout_ms` (integer, default: `30000`): Maximum time to wait.
|
|
210
|
+
* `max_output_bytes` (integer, optional): Maximum output bytes to return in the response.
|
|
211
|
+
|
|
212
|
+
* **Important**: If the wait timeout expires, `wait` returns the current status but **does not kill** the process. It continues running in the background.
|
|
213
|
+
* The effective wait duration is capped at 55 seconds to stay well under typical MCP request timeouts, even if a larger `timeout_ms` is requested.
|
|
214
|
+
* `wait` treats the tracked shell process exiting as completion. For normal process completion, stdout/stderr are drained before the response is returned. If descendant processes keep inherited pipes open after the tracked process exits, the server closes its drain tasks so `wait` can complete without blocking indefinitely.
|
|
215
|
+
|
|
216
|
+
### `stop`
|
|
217
|
+
Gracefully terminate or force kill a running process.
|
|
218
|
+
|
|
219
|
+
* **Parameters**:
|
|
220
|
+
* `process_id` (string, **required**): Unique identifier of the process.
|
|
221
|
+
* `signal` (string, default: `"SIGTERM"`): OS signal to send (e.g. `SIGTERM`, `SIGKILL`, `SIGINT`). Ignored on Windows.
|
|
222
|
+
* `force_after_ms` (integer, default: `3000`): Grace period before escalating to force kill (`SIGKILL`).
|
|
223
|
+
|
|
224
|
+
### `ps`
|
|
225
|
+
List all managed processes.
|
|
226
|
+
|
|
227
|
+
* **Parameters**:
|
|
228
|
+
* `include_completed` (boolean, default: `true`): If `false`, finished/stopped processes are excluded from the output.
|
|
229
|
+
* `limit` (integer, default: `50`): Maximum number of entries.
|
|
230
|
+
* **Returns**: `processes` — a list of process summary objects, each containing: `process_id`, `pid`, `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes`.
|
|
231
|
+
|
|
232
|
+
### Error Responses
|
|
233
|
+
|
|
234
|
+
All tools that accept a `process_id` parameter return a structured error dict when the ID is unknown, e.g. `{"process_id": "proc_abc123", "error": "Unknown process_id: proc_abc123"}`. Tools that accept `process_id` always include it in the response alongside the error.
|
|
235
|
+
|
|
236
|
+
### `cleanup`
|
|
237
|
+
Prune completed or stopped process records to free memory.
|
|
238
|
+
|
|
239
|
+
* **Parameters**:
|
|
240
|
+
* `completed_older_than_ms` (integer, default: `3600000`): Prunes completed processes older than this threshold (1 hour default).
|
|
241
|
+
* `stopped_older_than_ms` (integer, default: `3600000`): Prunes stopped, timed-out, or failed processes older than this threshold (1 hour default).
|
|
242
|
+
* **Returns**: `removed` — the count of process records that were pruned.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Sequence Number & Incremental Reads
|
|
247
|
+
|
|
248
|
+
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a sequence-based polling protocol:
|
|
249
|
+
|
|
250
|
+
1. Every output chunk appended to a process's ring buffer receives a unique, incremental sequence number (`seq`).
|
|
251
|
+
2. When calling `exec`, `read`, or `wait`, the response includes a `next_seq` value representing the index of the next chunk to be written.
|
|
252
|
+
3. To retrieve only *new* output, call `read` with `since_seq` set to the previously received `next_seq`.
|
|
253
|
+
4. Omitting `since_seq` returns the entire contents currently stored in the buffer (clamped by `max_output_bytes`).
|
|
254
|
+
5. If output exceeds the ring buffer's capacity between reads, older data is evicted and `since_seq` may no longer be available. In that case, `truncated` is set to `true` and the read returns data from the earliest retained sequence onward.
|
|
255
|
+
|
|
256
|
+
When a process exits normally, `exec`/`wait` responses include output drained through stdout/stderr EOF. If a descendant keeps inherited stdout/stderr open after the tracked process exits, the server stops waiting on those inherited pipes to avoid indefinite blocking; output written only by that descendant after the tracked process exits is not part of the managed process result.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Configuration Variables
|
|
261
|
+
|
|
262
|
+
Configure the server by setting these environment variables prior to launch:
|
|
263
|
+
|
|
264
|
+
| Environment Variable | Default Value | Description |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| `YIELDSHELL_DEFAULT_CWD` | Current directory | The fallback working directory for commands. |
|
|
267
|
+
| `YIELDSHELL_ALLOWED_CWDS` | *(none)* | A list of allowed directory paths separated by `os.pathsep` (e.g., `:` on UNIX, `;` on Windows). If set, all command execution paths must resolve inside one of these roots. |
|
|
268
|
+
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum capacity of the ring buffers for stdout/stderr. |
|
|
269
|
+
| `YIELDSHELL_MAX_PROCESSES` | `50` | Maximum concurrent running processes. Completed, stopped, timed-out, and failed processes do not count against this limit. Spawning a new command when this limit is reached will return `failed_to_start`. |
|
|
270
|
+
| `YIELDSHELL_DEFAULT_YIELD_MS` | `5000` | Fallback delay before auto-yielding. |
|
|
271
|
+
| `YIELDSHELL_MAX_YIELD_MS` | `300000` | The maximum allowed value for the `yield_ms` parameter. |
|
|
272
|
+
| `YIELDSHELL_DEFAULT_TIMEOUT_MS` | `0` | Default hard runtime limit (0 means no limit). |
|
|
273
|
+
| `YIELDSHELL_DENY_COMMAND_REGEX` | *(none)* | A regular expression pattern. Commands matching this pattern are blocked before starting. |
|
|
274
|
+
| `YIELDSHELL_ALLOW_COMMAND_REGEX` | *(none)* | A regular expression pattern. If set, only commands matching this pattern are permitted. |
|
|
275
|
+
| `YIELDSHELL_REDACT_ENV_REGEX` | `TOKEN\|KEY\|SECRET\|PASSWORD` | Regex to identify sensitive environment variable keys. Their values are redacted in stdout/stderr outputs. |
|
|
276
|
+
| `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS` | `KILLS_AGENT_PROCESS,MODIFIES_OS_SETTINGS,MODIFIES_OS_USER_SETTINGS,MODIFIES_PROTECTED_FILES,RUNS_INLINE_CODE` | Comma-separated list of `side_effects` enum names the server should reject. Names are case-sensitive. Surrounding whitespace is trimmed and empty entries are ignored. Invalid names cause startup to fail. Set to `,` (or any value that resolves to no entries) to clear the default blocklist. |
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Security Notes
|
|
281
|
+
|
|
282
|
+
* **Arbitrary Code Execution**: This server executes shell commands on the host system. Always run the server inside a container, sandbox, or isolated development VM.
|
|
283
|
+
* **Side-Effect Declarations**: Every `exec` call must declare its plausible side-effect categories via `side_effects`. By default, `KILLS_AGENT_PROCESS`, `MODIFIES_OS_SETTINGS`, `MODIFIES_OS_USER_SETTINGS`, `MODIFIES_PROTECTED_FILES`, and `RUNS_INLINE_CODE` are blocked. Operators can adjust the blocklist via `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS` (including cleared to `,` to disable every default). This is an explicit risk signal — it is not a complete sandbox, and LLM under-declaration remains possible.
|
|
284
|
+
* **Inline Code Execution**: The `RUNS_INLINE_CODE` default discourages agents from executing code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `ruby -e`, `perl -e`, shell heredocs piped into interpreters, or `curl ... | sh`). The safer pattern is to write the content to a reviewable workspace file and execute it in a small, inspectable step with explicit matching `side_effects`. Operators can override the default to permit the category.
|
|
285
|
+
* **OS User Settings Damage**: `MODIFIES_OS_USER_SETTINGS` covers commands that change user-level configuration such as shell rc files, XDG config directories, dotfiles, or per-user application preferences. This is distinct from `MODIFIES_OS_SETTINGS`, which covers broader OS-level configuration such as systemd units, kernel parameters, `/etc` files, and package manager system config. Blocked by default; operators can override.
|
|
286
|
+
* **Agent Process Termination**: `KILLS_AGENT_PROCESS` covers commands that may terminate the MCP client, agent, or related process running the agent workflow (e.g., `kill` commands targeting the agent PID, or commands that cause the agent to exit). This is distinct from `STOPS_OR_RESTARTS_SERVICES`, which covers OS-level services. Blocked by default; operators can override.
|
|
287
|
+
* **Path Validation**: CWD path verification uses absolute paths (`resolve()`), preventing path-traversal attacks (`../`) outside the allowed roots.
|
|
288
|
+
* **Additive Environments**: The `env` argument overlays existing env parameters. It merges with the parent process environment instead of completely replacing it, protecting critical OS vars.
|
|
289
|
+
* **Best-effort Redaction**: While values of variables matching `YIELDSHELL_REDACT_ENV_REGEX` are scrubbed from outputs, this is a best-effort system. Sensitive data printed through complex formats or argument lists might not be caught.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Platform Support
|
|
294
|
+
|
|
295
|
+
* **POSIX (Linux & macOS)**: Fully supported. Spawns processes in distinct sessions (`start_new_session=True`), allowing signals (`SIGTERM`/`SIGKILL`) to target the entire process group. This ensures child processes started by commands (such as npm dev tasks) are completely cleaned up.
|
|
296
|
+
* **Windows**: Supported with best-effort process group controls. Windows lacks native POSIX signals, meaning `stop` and `timeout_ms` act on the primary process, and child subprocesses might persist if they do not exit cleanly.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
MIT
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# YieldShell MCP
|
|
2
|
+
|
|
3
|
+
A drop-in shell MCP server that auto-yields long-running commands into managed background processes.
|
|
4
|
+
|
|
5
|
+
## Why Auto-Yielding?
|
|
6
|
+
|
|
7
|
+
Most shell tools present a frustrating choice: either block the LLM agent until the command finishes, or force the agent to decide upfront that a command should run in the background.
|
|
8
|
+
|
|
9
|
+
**YieldShell MCP** solves this by keeping normal foreground semantics for fast commands, then automatically promoting long-running commands into managed background processes after a brief delay (`yield_ms`, default: 5 seconds).
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
graph TD
|
|
13
|
+
A[exec_command] --> B["Wait for yield_ms (default: 5s)"]
|
|
14
|
+
B --> C{Is process still running?}
|
|
15
|
+
C -->|Yes| D["backgrounded<br>Returns process_id"]
|
|
16
|
+
C -->|No| E["completed<br>Returns full output"]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **Fast Commands** (e.g., `echo hello`, `ls`): Complete instantly, returning the output immediately.
|
|
20
|
+
- **Long-Running Commands** (e.g., `npm run dev`, `docker build`, `sleep 60`): Automatically yield control back to the agent with a `process_id` and a snapshot of initial output, letting the agent decide when to `read`, `wait`, or `stop` the process.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### From Registry (Recommended)
|
|
27
|
+
|
|
28
|
+
To run the published package via `uv`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv tool install mcp-yieldshell
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Local Development
|
|
35
|
+
|
|
36
|
+
To clone and run locally:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone <repo-url> && cd mcp-yieldshell
|
|
40
|
+
uv sync
|
|
41
|
+
uv run mcp-yieldshell
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## MCP Client Configuration
|
|
47
|
+
|
|
48
|
+
### Claude Desktop
|
|
49
|
+
|
|
50
|
+
To configure the server in Claude Desktop, add the configuration below to your Claude Desktop config file:
|
|
51
|
+
|
|
52
|
+
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
53
|
+
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
54
|
+
|
|
55
|
+
#### Production (via uvx)
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"yieldshell": {
|
|
61
|
+
"command": "uvx",
|
|
62
|
+
"args": ["mcp-yieldshell"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Production with Security Restrictions
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"yieldshell": {
|
|
74
|
+
"command": "uvx",
|
|
75
|
+
"args": ["mcp-yieldshell"],
|
|
76
|
+
"env": {
|
|
77
|
+
"YIELDSHELL_ALLOWED_CWDS": "/home/user/projects:/tmp/build",
|
|
78
|
+
"YIELDSHELL_DEFAULT_TIMEOUT_MS": "300000"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Local Development Setup
|
|
86
|
+
|
|
87
|
+
Replace `/path/to/mcp-yieldshell` with the absolute path to your cloned repository:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"yieldshell": {
|
|
93
|
+
"command": "uv",
|
|
94
|
+
"args": [
|
|
95
|
+
"--directory",
|
|
96
|
+
"/path/to/mcp-yieldshell",
|
|
97
|
+
"run",
|
|
98
|
+
"mcp-yieldshell"
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Cursor
|
|
106
|
+
|
|
107
|
+
To configure the server in Cursor:
|
|
108
|
+
1. Open **Cursor Settings** -> **Features** -> **MCP**.
|
|
109
|
+
2. Click **+ Add New MCP Server**.
|
|
110
|
+
3. Fill out the form:
|
|
111
|
+
- **Name**: `yieldshell`
|
|
112
|
+
- **Type**: `stdio`
|
|
113
|
+
- **Command**: `uvx mcp-yieldshell` (or `uv --directory /path/to/mcp-yieldshell run mcp-yieldshell` for local development)
|
|
114
|
+
|
|
115
|
+
### OpenCode
|
|
116
|
+
|
|
117
|
+
Add to your OpenCode MCP settings:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"yieldshell": {
|
|
123
|
+
"command": "uvx",
|
|
124
|
+
"args": ["mcp-yieldshell"]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Tool Reference
|
|
133
|
+
|
|
134
|
+
### `exec`
|
|
135
|
+
Execute a shell command. If the command runs longer than `yield_ms`, it yields a `process_id` and runs in the background.
|
|
136
|
+
|
|
137
|
+
* **Parameters**:
|
|
138
|
+
* `command` (string, **required**): The command string to execute in the shell.
|
|
139
|
+
* `side_effects` (array of string, **required**): The side-effect categories this command may plausibly have. Must contain at least one entry drawn from the enum below. Use `["NONE"]` for commands with no meaningful side effects. `NONE` is exclusive and must not be combined with any other category. The server rejects the call with `failed_to_start` if any declared category is configured as blocked.
|
|
140
|
+
* Allowed values: `CHANGES_NETWORK_CONFIGURATION`, `CHANGES_PACKAGES_OR_DEPENDENCIES`, `CONSUMES_SIGNIFICANT_RESOURCES`, `DELETES_FILES`, `EXPOSES_SECRETS`, `KILLS_AGENT_PROCESS`, `MAKES_NETWORK_REQUESTS`, `MODIFIES_OS_SETTINGS`, `MODIFIES_OS_USER_SETTINGS`, `MODIFIES_OUTSIDE_WORKSPACE`, `MODIFIES_PRODUCTION_SERVICES`, `MODIFIES_PROTECTED_FILES`, `MODIFIES_SECURITY_CONTROLS`, `MODIFIES_WORKSPACE_FILES`, `NONE`, `OTHER`, `RUNS_INLINE_CODE`, `RUNS_PRIVILEGED_COMMANDS`, `STOPS_OR_RESTARTS_SERVICES`, `UNKNOWN`, `USES_DESTRUCTIVE_GIT_OPERATION`.
|
|
141
|
+
* `RUNS_INLINE_CODE` is in the default blocklist. It covers commands that execute code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `curl ... | sh`). It does not cover simply creating a script or executable file unless the same command also executes inline code. The safer next action is to write the content to a reviewable workspace file and execute it in a small, inspectable step. Operators can unblock the category via `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=`.
|
|
142
|
+
* `cwd` (string, optional): Working directory for the command. Must be under allowed roots if `YIELDSHELL_ALLOWED_CWDS` is set. Defaults to `YIELDSHELL_DEFAULT_CWD`.
|
|
143
|
+
* `env` (object of string to string, optional): Additive environment variable overlay. Merged into the parent environment.
|
|
144
|
+
* `shell` (string, optional): Accepted but has no effect in v1. Commands always run via the platform's default shell.
|
|
145
|
+
* `stdin` (string, optional): Initial text input written to standard input immediately after spawning.
|
|
146
|
+
* `name` (string, optional): A human-readable label/name to identify this process.
|
|
147
|
+
* `yield_ms` (integer, optional): Milliseconds to wait before yielding execution to background. Clamped by `YIELDSHELL_MAX_YIELD_MS`. Defaults to `YIELDSHELL_DEFAULT_YIELD_MS` (5000ms).
|
|
148
|
+
* `timeout_ms` (integer, optional): Total execution runtime limit in milliseconds. Process is killed if it runs longer than this. Defaults to `YIELDSHELL_DEFAULT_TIMEOUT_MS` (0 = no limit).
|
|
149
|
+
* `max_output_bytes` (integer, optional): Maximum output bytes to capture in stdout/stderr ring buffers. Subject to `YIELDSHELL_MAX_OUTPUT_BYTES` cap.
|
|
150
|
+
|
|
151
|
+
* **Side-Effects Guide**:
|
|
152
|
+
* `side_effects` is required and must be a non-empty list. Declare every plausible side-effect category before running the command.
|
|
153
|
+
* `NONE` is exclusive and valid only when no meaningful side effect is expected. Use `["NONE"]` for read-only commands.
|
|
154
|
+
* The server rejects the call with `failed_to_start` if any declared category is blocked. Rejection messages name each blocked category, state that execution was stopped by policy before the process started, and provide a category-specific safer next action.
|
|
155
|
+
* Categories are case-sensitive and must use the canonical enum names listed above.
|
|
156
|
+
* **Discouraged**: executing code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `ruby -e`, `perl -e`, shell heredocs piped into interpreters, or `curl ... | sh`). Agents should prefer writing such content to a reviewable workspace file and executing it in a small, inspectable step with explicit matching `side_effects`. Declaring `RUNS_INLINE_CODE` is rejected under the default policy.
|
|
157
|
+
|
|
158
|
+
* **Side-Effect Examples**:
|
|
159
|
+
* Read-only command: `side_effects=["NONE"]`
|
|
160
|
+
* Workspace write: `side_effects=["MODIFIES_WORKSPACE_FILES"]`
|
|
161
|
+
* Dependency install: `side_effects=["CHANGES_PACKAGES_OR_DEPENDENCIES", "MAKES_NETWORK_REQUESTS"]`
|
|
162
|
+
* Network access: `side_effects=["MAKES_NETWORK_REQUESTS"]`
|
|
163
|
+
* Destructive file operations: `side_effects=["DELETES_FILES"]`
|
|
164
|
+
* Privileged command: `side_effects=["RUNS_PRIVILEGED_COMMANDS"]`
|
|
165
|
+
* Protected-file changes: `side_effects=["MODIFIES_PROTECTED_FILES"]`
|
|
166
|
+
* Inline code execution: prefer writing the content to a reviewable workspace file (for example `scripts/migrate.sql` or `tools/build.sh`) and run it in a small, inspectable step. Declaring `side_effects=["RUNS_INLINE_CODE"]` is rejected under the default policy; operators can clear that default with `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=`.
|
|
167
|
+
|
|
168
|
+
* **Output Statuses**:
|
|
169
|
+
* `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr.
|
|
170
|
+
* `backgrounded`: Process auto-yielded. Returns `process_id`, `pid`, a snapshot of initial stdout/stderr, `duration_ms`, `truncated`, and a `message` string describing that the process is running in the background.
|
|
171
|
+
* `timed_out`: Process exceeded `timeout_ms` and was terminated.
|
|
172
|
+
* `stopped`: Process was explicitly terminated.
|
|
173
|
+
* `failed_to_start`: Command could not be spawned (e.g., bad directory or policy violation).
|
|
174
|
+
* `failed`: An internal execution error occurred.
|
|
175
|
+
|
|
176
|
+
### `read`
|
|
177
|
+
Read stdout and/or stderr output from a running or completed background process.
|
|
178
|
+
|
|
179
|
+
* **Parameters**:
|
|
180
|
+
* `process_id` (string, **required**): Unique identifier of the process.
|
|
181
|
+
* `since_seq` (integer, optional): Return only output appended after this sequence number. Enables efficient incremental log polling.
|
|
182
|
+
* `max_output_bytes` (integer, optional): Clamps the response size. Defaults to the server cap.
|
|
183
|
+
* `streams` (string, default: `"both"`): The streams to read. Options: `"both"`, `"stdout"`, or `"stderr"`.
|
|
184
|
+
|
|
185
|
+
* **Returns**:
|
|
186
|
+
* `process_id`, `status`, `exit_code`, `signal`, `next_seq` (sequence index to use in subsequent `since_seq` reads), `truncated` flag. `stdout` and `stderr` text are included based on the `streams` filter — `"both"` includes both, `"stdout"` includes only `stdout`, and `"stderr"` includes only `stderr`.
|
|
187
|
+
|
|
188
|
+
### `write`
|
|
189
|
+
Write text input to the standard input (`stdin`) of a running process.
|
|
190
|
+
|
|
191
|
+
* **Parameters**:
|
|
192
|
+
* `process_id` (string, **required**): Unique identifier of the process.
|
|
193
|
+
* `input` (string, **required**): Text input to write.
|
|
194
|
+
* `newline` (boolean, default: `false`): If `true`, appends `\n` to the input.
|
|
195
|
+
|
|
196
|
+
### `wait`
|
|
197
|
+
Block execution until the process exits or the wait timeout expires. This allows the LLM to pause and await completion without spawning a new execution loop.
|
|
198
|
+
|
|
199
|
+
* **Parameters**:
|
|
200
|
+
* `process_id` (string, **required**): Unique identifier of the process.
|
|
201
|
+
* `timeout_ms` (integer, default: `30000`): Maximum time to wait.
|
|
202
|
+
* `max_output_bytes` (integer, optional): Maximum output bytes to return in the response.
|
|
203
|
+
|
|
204
|
+
* **Important**: If the wait timeout expires, `wait` returns the current status but **does not kill** the process. It continues running in the background.
|
|
205
|
+
* The effective wait duration is capped at 55 seconds to stay well under typical MCP request timeouts, even if a larger `timeout_ms` is requested.
|
|
206
|
+
* `wait` treats the tracked shell process exiting as completion. For normal process completion, stdout/stderr are drained before the response is returned. If descendant processes keep inherited pipes open after the tracked process exits, the server closes its drain tasks so `wait` can complete without blocking indefinitely.
|
|
207
|
+
|
|
208
|
+
### `stop`
|
|
209
|
+
Gracefully terminate or force kill a running process.
|
|
210
|
+
|
|
211
|
+
* **Parameters**:
|
|
212
|
+
* `process_id` (string, **required**): Unique identifier of the process.
|
|
213
|
+
* `signal` (string, default: `"SIGTERM"`): OS signal to send (e.g. `SIGTERM`, `SIGKILL`, `SIGINT`). Ignored on Windows.
|
|
214
|
+
* `force_after_ms` (integer, default: `3000`): Grace period before escalating to force kill (`SIGKILL`).
|
|
215
|
+
|
|
216
|
+
### `ps`
|
|
217
|
+
List all managed processes.
|
|
218
|
+
|
|
219
|
+
* **Parameters**:
|
|
220
|
+
* `include_completed` (boolean, default: `true`): If `false`, finished/stopped processes are excluded from the output.
|
|
221
|
+
* `limit` (integer, default: `50`): Maximum number of entries.
|
|
222
|
+
* **Returns**: `processes` — a list of process summary objects, each containing: `process_id`, `pid`, `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes`.
|
|
223
|
+
|
|
224
|
+
### Error Responses
|
|
225
|
+
|
|
226
|
+
All tools that accept a `process_id` parameter return a structured error dict when the ID is unknown, e.g. `{"process_id": "proc_abc123", "error": "Unknown process_id: proc_abc123"}`. Tools that accept `process_id` always include it in the response alongside the error.
|
|
227
|
+
|
|
228
|
+
### `cleanup`
|
|
229
|
+
Prune completed or stopped process records to free memory.
|
|
230
|
+
|
|
231
|
+
* **Parameters**:
|
|
232
|
+
* `completed_older_than_ms` (integer, default: `3600000`): Prunes completed processes older than this threshold (1 hour default).
|
|
233
|
+
* `stopped_older_than_ms` (integer, default: `3600000`): Prunes stopped, timed-out, or failed processes older than this threshold (1 hour default).
|
|
234
|
+
* **Returns**: `removed` — the count of process records that were pruned.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Sequence Number & Incremental Reads
|
|
239
|
+
|
|
240
|
+
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a sequence-based polling protocol:
|
|
241
|
+
|
|
242
|
+
1. Every output chunk appended to a process's ring buffer receives a unique, incremental sequence number (`seq`).
|
|
243
|
+
2. When calling `exec`, `read`, or `wait`, the response includes a `next_seq` value representing the index of the next chunk to be written.
|
|
244
|
+
3. To retrieve only *new* output, call `read` with `since_seq` set to the previously received `next_seq`.
|
|
245
|
+
4. Omitting `since_seq` returns the entire contents currently stored in the buffer (clamped by `max_output_bytes`).
|
|
246
|
+
5. If output exceeds the ring buffer's capacity between reads, older data is evicted and `since_seq` may no longer be available. In that case, `truncated` is set to `true` and the read returns data from the earliest retained sequence onward.
|
|
247
|
+
|
|
248
|
+
When a process exits normally, `exec`/`wait` responses include output drained through stdout/stderr EOF. If a descendant keeps inherited stdout/stderr open after the tracked process exits, the server stops waiting on those inherited pipes to avoid indefinite blocking; output written only by that descendant after the tracked process exits is not part of the managed process result.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Configuration Variables
|
|
253
|
+
|
|
254
|
+
Configure the server by setting these environment variables prior to launch:
|
|
255
|
+
|
|
256
|
+
| Environment Variable | Default Value | Description |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `YIELDSHELL_DEFAULT_CWD` | Current directory | The fallback working directory for commands. |
|
|
259
|
+
| `YIELDSHELL_ALLOWED_CWDS` | *(none)* | A list of allowed directory paths separated by `os.pathsep` (e.g., `:` on UNIX, `;` on Windows). If set, all command execution paths must resolve inside one of these roots. |
|
|
260
|
+
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum capacity of the ring buffers for stdout/stderr. |
|
|
261
|
+
| `YIELDSHELL_MAX_PROCESSES` | `50` | Maximum concurrent running processes. Completed, stopped, timed-out, and failed processes do not count against this limit. Spawning a new command when this limit is reached will return `failed_to_start`. |
|
|
262
|
+
| `YIELDSHELL_DEFAULT_YIELD_MS` | `5000` | Fallback delay before auto-yielding. |
|
|
263
|
+
| `YIELDSHELL_MAX_YIELD_MS` | `300000` | The maximum allowed value for the `yield_ms` parameter. |
|
|
264
|
+
| `YIELDSHELL_DEFAULT_TIMEOUT_MS` | `0` | Default hard runtime limit (0 means no limit). |
|
|
265
|
+
| `YIELDSHELL_DENY_COMMAND_REGEX` | *(none)* | A regular expression pattern. Commands matching this pattern are blocked before starting. |
|
|
266
|
+
| `YIELDSHELL_ALLOW_COMMAND_REGEX` | *(none)* | A regular expression pattern. If set, only commands matching this pattern are permitted. |
|
|
267
|
+
| `YIELDSHELL_REDACT_ENV_REGEX` | `TOKEN\|KEY\|SECRET\|PASSWORD` | Regex to identify sensitive environment variable keys. Their values are redacted in stdout/stderr outputs. |
|
|
268
|
+
| `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS` | `KILLS_AGENT_PROCESS,MODIFIES_OS_SETTINGS,MODIFIES_OS_USER_SETTINGS,MODIFIES_PROTECTED_FILES,RUNS_INLINE_CODE` | Comma-separated list of `side_effects` enum names the server should reject. Names are case-sensitive. Surrounding whitespace is trimmed and empty entries are ignored. Invalid names cause startup to fail. Set to `,` (or any value that resolves to no entries) to clear the default blocklist. |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Security Notes
|
|
273
|
+
|
|
274
|
+
* **Arbitrary Code Execution**: This server executes shell commands on the host system. Always run the server inside a container, sandbox, or isolated development VM.
|
|
275
|
+
* **Side-Effect Declarations**: Every `exec` call must declare its plausible side-effect categories via `side_effects`. By default, `KILLS_AGENT_PROCESS`, `MODIFIES_OS_SETTINGS`, `MODIFIES_OS_USER_SETTINGS`, `MODIFIES_PROTECTED_FILES`, and `RUNS_INLINE_CODE` are blocked. Operators can adjust the blocklist via `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS` (including cleared to `,` to disable every default). This is an explicit risk signal — it is not a complete sandbox, and LLM under-declaration remains possible.
|
|
276
|
+
* **Inline Code Execution**: The `RUNS_INLINE_CODE` default discourages agents from executing code supplied inline to an interpreter or shell (e.g. `python -c`, `node -e`, `ruby -e`, `perl -e`, shell heredocs piped into interpreters, or `curl ... | sh`). The safer pattern is to write the content to a reviewable workspace file and execute it in a small, inspectable step with explicit matching `side_effects`. Operators can override the default to permit the category.
|
|
277
|
+
* **OS User Settings Damage**: `MODIFIES_OS_USER_SETTINGS` covers commands that change user-level configuration such as shell rc files, XDG config directories, dotfiles, or per-user application preferences. This is distinct from `MODIFIES_OS_SETTINGS`, which covers broader OS-level configuration such as systemd units, kernel parameters, `/etc` files, and package manager system config. Blocked by default; operators can override.
|
|
278
|
+
* **Agent Process Termination**: `KILLS_AGENT_PROCESS` covers commands that may terminate the MCP client, agent, or related process running the agent workflow (e.g., `kill` commands targeting the agent PID, or commands that cause the agent to exit). This is distinct from `STOPS_OR_RESTARTS_SERVICES`, which covers OS-level services. Blocked by default; operators can override.
|
|
279
|
+
* **Path Validation**: CWD path verification uses absolute paths (`resolve()`), preventing path-traversal attacks (`../`) outside the allowed roots.
|
|
280
|
+
* **Additive Environments**: The `env` argument overlays existing env parameters. It merges with the parent process environment instead of completely replacing it, protecting critical OS vars.
|
|
281
|
+
* **Best-effort Redaction**: While values of variables matching `YIELDSHELL_REDACT_ENV_REGEX` are scrubbed from outputs, this is a best-effort system. Sensitive data printed through complex formats or argument lists might not be caught.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Platform Support
|
|
286
|
+
|
|
287
|
+
* **POSIX (Linux & macOS)**: Fully supported. Spawns processes in distinct sessions (`start_new_session=True`), allowing signals (`SIGTERM`/`SIGKILL`) to target the entire process group. This ensures child processes started by commands (such as npm dev tasks) are completely cleaned up.
|
|
288
|
+
* **Windows**: Supported with best-effort process group controls. Windows lacks native POSIX signals, meaning `stop` and `timeout_ms` act on the primary process, and child subprocesses might persist if they do not exit cleanly.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT
|
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
7
|
|
|
8
|
+
from .types import DEFAULT_BLOCKED_SIDE_EFFECTS, SideEffect
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class Config:
|
|
10
12
|
def __init__(self) -> None:
|
|
@@ -37,6 +39,9 @@ class Config:
|
|
|
37
39
|
os.environ.get("YIELDSHELL_REDACT_ENV_REGEX", ""),
|
|
38
40
|
r"TOKEN|KEY|SECRET|PASSWORD",
|
|
39
41
|
)
|
|
42
|
+
self.blocked_side_effects: frozenset[SideEffect] = _parse_blocked_side_effects(
|
|
43
|
+
os.environ.get("MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS", "")
|
|
44
|
+
)
|
|
40
45
|
|
|
41
46
|
|
|
42
47
|
def _parse_pathsep(value: str) -> list[str]:
|
|
@@ -64,3 +69,35 @@ def _parse_regex_required(value: str, default: str) -> re.Pattern[str]:
|
|
|
64
69
|
if not value:
|
|
65
70
|
return re.compile(default)
|
|
66
71
|
return re.compile(value)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_blocked_side_effects(value: str) -> frozenset[SideEffect]:
|
|
75
|
+
"""Parse ``MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS`` into a frozenset.
|
|
76
|
+
|
|
77
|
+
Empty entries are ignored. Surrounding whitespace is trimmed. Names are
|
|
78
|
+
case-sensitive. Invalid names raise ``ValueError`` with a clear message.
|
|
79
|
+
"""
|
|
80
|
+
if value is None or not value.strip():
|
|
81
|
+
return DEFAULT_BLOCKED_SIDE_EFFECTS
|
|
82
|
+
|
|
83
|
+
valid_names = {member.name for member in SideEffect}
|
|
84
|
+
blocked: set[SideEffect] = set()
|
|
85
|
+
invalid: list[str] = []
|
|
86
|
+
for entry in value.split(","):
|
|
87
|
+
name = entry.strip()
|
|
88
|
+
if not name:
|
|
89
|
+
continue
|
|
90
|
+
if name in valid_names:
|
|
91
|
+
blocked.add(SideEffect[name])
|
|
92
|
+
else:
|
|
93
|
+
invalid.append(name)
|
|
94
|
+
|
|
95
|
+
if invalid:
|
|
96
|
+
names_list = ", ".join(repr(n) for n in invalid)
|
|
97
|
+
valid_list = ", ".join(sorted(valid_names))
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS contains invalid value(s): "
|
|
100
|
+
f"{names_list}. Valid values (case-sensitive): {valid_list}."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return frozenset(blocked)
|