mcp-pwsh
by Am6er
README.md
# mcp-pwsh
A persistent, pooled **PowerShell MCP server**. It gives an MCP client (Claude
Desktop, or any [Model Context Protocol](https://modelcontextprotocol.io) host)
a pool of long-lived, named **PowerShell 7** sessions with clean `stdin → stdout`
I/O — like keeping several real consoles open, instead of firing one-shot
commands.
## Why
A naive "run a PowerShell command" tool spawns a fresh process every call, so
state does not survive (cwd, variables, imported modules), and the output comes
back wrapped in host noise. `mcp-pwsh` instead keeps one real `pwsh` process per
**named session**:
- **Persistent state** — `cwd`, variables, `$env:`, and imported modules survive
between calls within a session.
- **Clean output** — a bootstrap loop reads commands with `[Console]::In.ReadLine()`
(no prompt, no echo) and delimits each result with a runtime marker, so you get
exactly the command's output plus its exit code — no `PS>` noise.
- **Named session pool (lanes)** — each session is an isolated lane with its own
process, state, and lock. Different sessions run independently, so two tasks can
work at once without corrupting each other's state.
- **No escaping games** — commands are sent base64-encoded over one line, so
quoting, newlines, and multi-line scripts just work.
## Install
```sh
pip install mcp-pwsh
```
Requires **Python 3.9+** and **PowerShell 7** (`pwsh`). On Windows the default
`pwsh` location is auto-detected; otherwise `pwsh` is looked up on `PATH`.
## Configure in an MCP client
Using the installed console script (recommended):
```json
{
"mcpServers": {
"pwsh": {
"command": "mcp-pwsh"
}
}
}
```
Or via the module (useful to pin a specific interpreter):
```json
{
"mcpServers": {
"pwsh": {
"command": "C:\\path\\to\\python.exe",
"args": ["-m", "mcp_pwsh"]
}
}
}
```
## Tools
| Tool | Purpose |
| --- | --- |
| `pwsh(command, session, timeout=60)` | Run a command in the named session; returns clean output + exit code. `session` is your chat's stable id — reuse it for every call. |
| `pwsh_list()` | List live sessions with owner id, pid, busy/idle, idle time, creation time, and last command. |
| `pwsh_close(session)` | Kill and remove one session. Call it when your task is done to free the lane. |
| `pwsh_reset(session)` | Restart one session's process (state cleared) — use if a command hung. |
| `pwsh_kill_all()` | Fallback: remove every session. |
**Discipline:** pick one stable `session` id per chat, reuse it, and
`pwsh_close` it when finished. Use `pwsh_list` to see lingering sessions and
`pwsh_kill_all` only as a last resort.
## Configuration (environment variables)
| Variable | Default | Meaning |
| --- | --- | --- |
| `MCP_PWSH_EXE` | auto-detected `pwsh` | Path to the PowerShell 7 executable. |
| `MCP_PWSH_CWD` | user home directory | Starting working directory for new sessions. |
| `MCP_PWSH_TIMEOUT_SEC` | `60` | Default per-command timeout (seconds). |
## How it works
Each session launches `pwsh` running a small bootstrap loop (passed via
`-EncodedCommand`). The loop reads `"<id> <base64-command>"` lines from stdin,
runs the command in a persistent scope, and prints `<<<MCPEND:<id>:<exitcode>>>>`.
Because the full marker is never present in the input, it can never false-match,
and because input is read (not typed into an interactive prompt) there is no echo
or prompt to strip. The bootstrap also restores a sane `PATHEXT`/`ComSpec` in
case the host launched the process with a stripped environment, so native
executables run normally.
## Self-test
Runs a local check of session isolation, listing, close, and kill-all (needs
`pwsh` available; no MCP host required):
```sh
python -m mcp_pwsh --selftest
```
## License
[GPL-2.0](LICENSE)
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues