<p align="center">
<img src=".github/assets/fairy.png" alt="Winx fairy mascot" width="160" />
</p>
# ✨ Winx - MCP Server for Shell & Coding Agents ✨
<p align="center">
<strong>🦀 Native Rust implementation inspired by WCGW, built for local code-agent workflows</strong>
</p>
<p align="center">
<img src="https://img.shields.io/badge/language-Rust-orange?style=flat&logo=rust" alt="Language" />
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat" alt="License" />
<img src="https://img.shields.io/badge/MCP-compatible-purple?style=flat" alt="MCP" />
<img src="https://img.shields.io/badge/transport-stdio-2f855a?style=flat" alt="stdio transport" />
</p>
<p align="center">
<em>A local MCP server you can hand to a coding agent and stop worrying about the shell.</em>
</p>
Winx is the MCP server I wanted while running Claude, Codex, and friends against real repos: one process that handles
the shell, file IO, and PTY-backed interactive sessions, written in Rust so it doesn't fight you on stdio.
It started as a Rust port of [WCGW](https://github.com/rusiaaman/wcgw) but isn't a Python wrapper. Everything runs on a
real PTY (via `portable-pty`), `cd` actually sticks, `Ctrl+C` actually interrupts, and background shells survive
long-running TUIs without leaking output buffers into your token budget.
## What you get
- A stateful bash session per thread with proper PTY semantics - foreground, background, status checks, text input,
Enter/Ctrl-C/Ctrl-D, raw ASCII. Multiline scripts and top-level `command` shorthand both work; NUL bytes are
rejected before they reach the shell.
- Workspaces with three modes: `wcgw` (full access), `architect` (read-only), `code_writer` (allowlist of commands and
write globs). The command allowlist is parsed with tree-sitter, so it checks **every** command on the line -
pipelines, `&&`/`||`/`;`, command substitution, subshells - not just the first word, and can't be bypassed with
`ls && curl … | sh` or `ls $(rm …)`.
- A resilient PTY: a shell that won't return to a prompt (even after Ctrl-C) is auto-reset at the same cwd/mode, child
processes are reaped on drop, and prompt detection is robust to a custom `PS1`. Opt into `zsh` with `WINX_SHELL=zsh`.
- File reads with WCGW-style line ranges (`file.rs:10-40`, `file.rs:10-`, `file.rs:-40`). Active files are tracked
and prioritized in the repository context across calls.
- File writes and SEARCH/REPLACE edits that survive ambiguous matches, indentation drift, and the usual unicode
quote-mismatches from LLMs. Writes are blocked when the file hasn't been read or the cached content is stale, the
success message shows a compact diff of what changed, and recent edits are reversible with `UndoEdit`.
`MultiFileEdit` applies a change across several files all-or-nothing (validated in memory first, so a failure on the
last file leaves the earlier ones untouched).
- Tree-sitter code navigation via `CodeMap`: a token-budgeted symbol map of a file or the whole repo, or a
definition/reference lookup for a symbol name - the semantic view that plain `grep` can't give you, across 11
languages.
- `ContextSave` for handing a task summary plus its files to the next session - including workspace context, active
files, git status/diff, and terminal sharing for proper resumption. Resuming reopens the saved project root and
token-caps the restored memory so it never overflows the context window.
- `ReadImage` so multimodal clients can pull screenshots, mockups, error PNGs, etc.
- Clean, token-aware shell output: cursor/ANSI noise from interactive programs (REPLs, progress
bars) is rendered away through a terminal emulator, and mechanical repetition is collapsed
losslessly (`line [winx: ×N]`) so build/install logs don't blow your context budget. Toggle the
collapsing with `WINX_NO_COMPRESS`. When output still overflows the cap, the dropped head is streamed
to a scratch file under `.winx/scratch/` the agent can re-read, instead of being lost.
- Secret redaction on by default: provider API keys, JWTs, PEM private-key blocks and `user:pass@` URLs
are scrubbed from **all** tool output and saved memory before they reach the model (disable with
`WINX_NO_REDACT=1`). An opt-in Landlock sandbox (`WINX_SANDBOX=1`, Linux) adds a kernel-enforced second
layer that confines writes to the workspace and hides the home directory.
- Two transports: **stdio** for local clients, plus an optional token-gated **Streamable HTTP** server
(`winx serve --http`) for remote MCP clients like ChatGPT - see
[Remote access](#remote-access-chatgpt--other-remote-mcp-clients).
## MCP Tools
| Tool | What it does |
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Initialize` | Boots the workspace, picks the mode, hands you a `thread_id`. Call this first or everything else errors out. With no workspace path it spins up a scratch playground; resuming a task (`task_id_to_resume`) reopens its saved project root. |
| `BashCommand` | Runs commands, polls long-running ones, sends Enter/Ctrl-C, drives TUIs. Supports `is_background`, `status_check`, `send_text`, `send_specials`, `send_ascii`, `allow_multi`, plus `screen` (a stable point-in-time frame of an interactive TUI with the cursor position; pass `diff:true` for only the lines that changed since your last look) and `wait_for_turn` (block until the TUI is ready for input, via per-app or configurable recognizers). When a foreground command finishes, the status line reports its real `exit code` (parsed from the prompt marker), so failures surface without grepping stderr. |
| `ReadFiles` | One or many files, with line numbers. Append `:10-40` to a path for a range. When the token budget is hit it tells you the exact line + `file:N-M` syntax to resume from instead of silently dropping the tail. |
| `FileWriteOrEdit` | Full overwrites or SEARCH/REPLACE blocks (with optional `@start-end` line anchors to pin a repeated block). Validates file read coverage and freshness before writing, reports any fuzzy tolerances it had to apply, then runs a tree-sitter syntax check (18+ languages) and points at the offending line with a snippet. The success message includes a compact diff of what changed. |
| `MultiFileEdit` | Edits several files all-or-nothing: every file's edit is validated and computed in memory first, and only if all succeed is anything written - so a SEARCH that fails to match in the last file leaves the earlier ones untouched. For a single file use `FileWriteOrEdit`. |
| `UndoEdit` | Reverts a file to its content before the last `FileWriteOrEdit`/`MultiFileEdit` this session (per-file, last ~10 edits kept in memory). Refused if the file changed on disk since your edit; a brand-new file's creation isn't undoable. |
| `ContextSave` | Dumps task description + file globs into a single text file with workspace context, active files, and git status/diff for clean handoff and task resumption. |
| `ReadImage` | Returns a native MCP image content block (not base64 as text), so multimodal models actually see the image. Confined to the workspace (like `ReadFiles`) and size-capped. |
| `CodeMap` | Tree-sitter code navigation, in one tool with two `operation`s. `outline`: a symbol map (functions, types, methods, ...) - a file returns its definitions, a directory (or empty) a relevance-ranked, token-budgeted repo symbol map, in 11 languages. `references`: where a `name` is defined and used (called) across the repo, counting only real identifier occurrences (never inside strings/comments, unlike grep), definitions first. For plain-text/regex search and file discovery, just use `rg`/`fd`/`grep` via `BashCommand`. |
## Search/Replace editing
Standard block syntax:
```text
<<<<<<< SEARCH
old content
=======
new content
>>>>>>> REPLACE
```
Things the matcher forgives so you don't have to babysit the model:
- atomic: ambiguous or missing matches abort without touching the file
- adjusts replacement indentation when the LLM gets the leading whitespace wrong
- strips `ReadFiles` line numbers if they leak into a SEARCH block
- normalizes the usual "smart quote" / em-dash / ellipsis substitutions
- uses neighboring blocks to disambiguate when the same snippet appears twice
- single-line substring edits work - you don't need the whole line in SEARCH
- retries once with `\"` unescaped when the model over-escapes quotes in SEARCH
- refuses edits that only matched after too much fuzzy fixup, and rejects blocks
that match in too many places - so you re-read instead of corrupting the file
- anchor a block to a line number to pin one of several identical snippets -
`<<<<<<< SEARCH @42` (or a range `@42-50`); a stale anchor falls back to the
normal search, so it never fails an otherwise-valid edit
- tells you on success which tolerances it had to apply (so you learn your
SEARCH drifted), and on a miss how close the nearest match was, with `~`
marking the lines that diverged
## Install
```bash
cargo install winx-code-agent
```
Binary lands in `~/.cargo/bin` - every config snippet below assumes that's on `$PATH`. If your MCP client launches with
a sterile env, swap `winx-code-agent` for the absolute path (`which winx-code-agent`).
Needs Rust 1.75+, Linux/macOS/WSL2, and a real terminal (any modern one - Winx spawns its own PTY).
<details>
<summary><b>Claude Code (CLI)</b></summary>
One-liner via the CLI (stdio is the default transport):
```bash
claude mcp add winx -- winx-code-agent
```
Or drop a `.mcp.json` in your project root:
```json
{
"mcpServers": {
"winx": {
"command": "winx-code-agent",
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
</details>
<details>
<summary><b>Claude Desktop</b></summary>
Add to your config file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
```json
{
"mcpServers": {
"winx": {
"command": "winx-code-agent",
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
Restart Claude Desktop after saving.
</details>
<details>
<summary><b>Codex (OpenAI CLI)</b></summary>
One-liner:
```bash
codex mcp add winx -- winx-code-agent
```
Or edit `~/.codex/config.toml`:
```toml
[mcp_servers.winx]
command = "winx-code-agent"
env = { RUST_LOG = "winx_code_agent=info" }
```
</details>
<details>
<summary><b>Cursor</b></summary>
Add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` for project-local):
```json
{
"mcpServers": {
"winx": {
"command": "winx-code-agent",
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
</details>
<details>
<summary><b>VS Code (Copilot Chat / MCP)</b></summary>
Add to `.vscode/mcp.json`:
```json
{
"servers": {
"winx": {
"type": "stdio",
"command": "winx-code-agent"
}
}
}
```
</details>
<details>
<summary><b>Zed</b></summary>
Add to your Zed settings (`~/.config/zed/settings.json`):
```json
{
"context_servers": {
"winx": {
"source": "custom",
"command": "winx-code-agent",
"args": [],
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
</details>
<details>
<summary><b>Windsurf</b></summary>
Add to `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"winx": {
"command": "winx-code-agent",
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
</details>
<details>
<summary><b>OpenCode</b></summary>
Add to `opencode.json`:
```json
{
"mcp": {
"winx": {
"type": "local",
"command": ["winx-code-agent"],
"enabled": true,
"environment": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
</details>
<details>
<summary><b>Gemini CLI</b></summary>
Add to `~/.gemini/settings.json`:
```json
{
"mcpServers": {
"winx": {
"command": "winx-code-agent",
"args": [],
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
</details>
<details>
<summary><b>agy (Google Antigravity CLI)</b></summary>
`agy` is Google's new Gemini-powered CLI (Go binary, usually at `~/.local/bin/agy`). No `mcp add` subcommand yet - it
reads MCP servers from JSON.
Edit `~/.gemini/config/mcp_config.json` (also `~/.gemini/antigravity/mcp_config.json` if you run the Antigravity IDE
alongside):
```json
{
"mcpServers": {
"winx": {
"command": "winx-code-agent",
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
If `winx-code-agent` is not on the agy process `$PATH`, swap `command` for the absolute path (`~/.cargo/bin/winx-code-agent` after `cargo install winx-code-agent`).
</details>
<details>
<summary><b>Continue.dev</b></summary>
Add to your `~/.continue/config.yaml`:
```yaml
mcpServers:
- name: winx
command: winx-code-agent
env:
RUST_LOG: winx_code_agent=info
```
</details>
<details>
<summary><b>Kiro</b></summary>
Add to `~/.kiro/settings/mcp.json`:
```json
{
"mcpServers": {
"winx": {
"command": "winx-code-agent",
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
</details>
<details>
<summary><b>Warp</b></summary>
**Settings → MCP Servers → Add MCP Server**:
```json
{
"winx": {
"command": "winx-code-agent",
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
```
</details>
<details>
<summary><b>Roo Code</b></summary>
Add to your Roo Code MCP config:
```json
{
"mcpServers": {
"winx": {
"type": "stdio",
"command": "winx-code-agent"
}
}
}
```
</details>
<details>
<summary><b>Other clients (generic stdio)</b></summary>
Any client that speaks stdio MCP works with this shape:
```json
{
"mcpServers": {
"winx": {
"command": "winx-code-agent",
"args": [],
"env": { "RUST_LOG": "winx_code_agent=info" }
}
}
}
```
If your client launches Winx with an empty `$PATH`, swap `command` for the absolute path (
`~/.cargo/bin/winx-code-agent`).
</details>
<details>
<summary><b>Build from source</b></summary>
For unreleased changes or a custom build:
```bash
git clone https://github.com/gabrielmaialva33/winx-code-agent.git
cd winx-code-agent
cargo install --path .
```
Or run it without installing:
```bash
cargo run --release
```
</details>
### Check it's wired up
List MCP tools in your client. You should see nine entries: `Initialize`, `BashCommand`, `ReadFiles`, `FileWriteOrEdit`,
`MultiFileEdit`, `UndoEdit`, `ContextSave`, `ReadImage`, `CodeMap`. The first call always has to be `Initialize`; Winx tracks workspace + mode per thread.
## Remote access (ChatGPT & other remote MCP clients)
By default Winx speaks MCP over **stdio** - the local transport every desktop client (Claude Desktop, Cursor, VS Code)
uses. For clients that live in the cloud and can't reach your machine over stdio - like ChatGPT's developer-mode custom
connectors - Winx can also serve MCP over **Streamable HTTP**:
```bash
winx serve --http --bind 127.0.0.1:8000 --token "$(openssl rand -hex 24)"
```
The MCP protocol is served at `/mcp`. Every request must carry the token in the `Authorization: Bearer <token>` header
(header-only - a `?token=` query parameter would leak the secret into proxy/tunnel access logs and browser history).
Without a token the server refuses to start - serving a shell over the network without auth is remote code execution
waiting to happen. The endpoint also caps request bodies (64 MB) and times out stuck requests (120 s).
| Flag | Purpose |
|------------------|-------------------------------------------------------------------------------------------------|
| `--http` | Serve over Streamable HTTP instead of stdio. |
| `--bind` | Listen address. Defaults to `127.0.0.1:8000`. Keep it on loopback. |
| `--token` | Shared secret required on every request. Falls back to the `WINX_HTTP_TOKEN` env var. |
| `--allowed-host` | Extra `Host` authority to accept (your tunnel hostname). Repeatable. Loopback is always allowed. |
Remote clients run in the cloud, so the endpoint has to be reachable over HTTPS - put a tunnel in front of the loopback
listener and allow its hostname through the built-in DNS-rebinding guard:
```bash
# 1. tunnel first, to learn the public hostname
cloudflared tunnel --url http://localhost:8000
# -> https://<random>.trycloudflare.com
# 2. start Winx, allowing that host
winx serve --http --bind 127.0.0.1:8000 \
--token "$(openssl rand -hex 24)" \
--allowed-host <random>.trycloudflare.com
```
In ChatGPT (Settings → Apps → Advanced → **Developer mode**), add a connector with:
- **URL**: `https://<random>.trycloudflare.com/mcp`
- **Authentication**: bearer / API-key token set to `<your-token>`, so the connector sends it as
`Authorization: Bearer <your-token>` (the token is no longer accepted in the URL)
Remote clients are effectively **stateless** - they don't reuse the MCP session between tool calls - so the HTTP
transport shares one shell session across all requests: the shell `Initialize` creates stays alive for the lifetime of
the server, and later `BashCommand` calls find it. Reuse the same `thread_id` across calls.
> [!WARNING]
> The HTTP transport puts arbitrary shell and file access on the network. Anyone with the token (and URL) gets a shell
> on your machine as your user - and not just inside the workspace, since `BashCommand` in `wcgw` mode isn't
> path-restricted. Bind to loopback, keep it behind an authenticated tunnel, prefer `architect`/`code_writer` mode or a
> container, and shut it down when you're done.
## Environment variables
All optional - Winx works out of the box without any of these.
| Variable | Effect |
|----------|--------|
| `RUST_LOG` | Log verbosity, e.g. `winx_code_agent=info`. At `info` you get the per-call **audit trail** (tool name, arg summary, duration, ok/error). |
| `WINX_HTTP_TOKEN` | Shared secret for the HTTP transport, used if `--token` isn't passed (see [Remote access](#remote-access-chatgpt--other-remote-mcp-clients)). |
| `WINX_NO_COMPRESS` | Set to `1` to disable output compression and see raw, uncollapsed shell output (the `[winx: ×N]` collapsing is on by default). |
| `WINX_NO_REDACT` | Set to `1` to disable secret redaction. By default winx scrubs high-confidence credentials (provider API keys, JWTs, PEM private keys, `user:pass@` URLs) from all tool output and saved memory, replacing each with `[REDACTED:<rule>]`. Turn this off only when you knowingly need a raw value. |
| `WINX_SANDBOX` | Set to `1` to enable an opt-in Landlock filesystem sandbox (Linux 5.13+, EXPERIMENTAL). Confines winx and its shell to write only the workspace (the cwd at startup) plus `/tmp`, and makes the home directory unreadable, so a manipulated agent can't read `~/.ssh`/`~/.aws` or modify files outside the project. Coarse and best-effort: a command needing a path outside the allowlist fails. Degrades to a warning (unsandboxed) on older kernels. |
| `WINX_SANDBOX_RO_PATHS` / `WINX_SANDBOX_RW_PATHS` | `:`-separated absolute paths to additionally allow read-only / read-write under `WINX_SANDBOX` (e.g. `WINX_SANDBOX_RO_PATHS=$HOME/.cargo:$HOME/.rustup` so cargo still works). |
| `WINX_TURN_RECOGNIZER_CONFIG` | JSON `{"busy":[…],"awaiting_input":[…],"awaiting_approval":[…]}` of marker strings/regexes. With `recognizer:"configurable"`, lets `wait_for_turn` drive an arbitrary TUI without bespoke code. |
| `WINX_CODING_TOKEN_BUDGET` / `WINX_NONCODING_TOKEN_BUDGET` | Override the per-file token budget for `ReadFiles` (and saved memory) - raise it for large-context models. Defaults: `24000` / `8000`. |
| `WINX_KEEP_TAIL_PIPE` | Set to `1` to keep a trailing `\| tail …` instead of stripping it. Winx truncates output server-side, so by default it drops a redundant trailing `tail` (wcgw parity). |
| `WINX_USE_SCREEN` / `WINX_ATTACH_TERMINAL` | Run the shell inside `screen`/`tmux` so you can attach to the live session. Set to `screen`, `tmux`, or any truthy value; Winx prints an attach hint on `Initialize`. |
| `WINX_OPEN_CONTEXT` | Set to `1` to open the saved context file in your default app after `ContextSave`. |
| `WINX_SHELL` | Set to `zsh` to run the session under zsh instead of bash (opt-in; bash stays the default). Falls back to bash if zsh isn't on `PATH` or the mode is restricted. |
| `WINX_SERVER_INSTRUCTIONS` | Extra operator instructions appended to every `Initialize` response (e.g. house rules for the agent). |
## Hacking on it
```bash
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
```
CI runs the same three. If you touch `src/state/pty.rs` or anything in `src/tools/bash_command.rs`, the regression suite
at `tests/bash_pty_regression_test.rs` is what protects against the usual TUI/PTY foot-guns - run it first.
Robustness is also fuzzed and model-checked:
- **proptest** feeds arbitrary/adversarial bytes into the live terminal emulator, the ANSI stripper, and the exit-code
parser, asserting they never panic and stay within the viewport. (This is how we found - and worked around - a vt100
underflow on tiny grids and a reflow panic on column shrink that would otherwise crash the `panic = "abort"` release.)
- **loom** exhaustively model-checks the session pin counter (the lock-free guard that keeps an in-flight session from
being LRU-evicted) across every thread interleaving. It's behind a feature so it doesn't perturb the normal build:
```bash
cargo test --features loom --lib loom_
```
## A note on security
By default this is a local (stdio) MCP server. Anything connected to it can read files, edit files, and run shell
commands inside the workspace - same blast radius as letting the model into your terminal. The optional HTTP transport
(`--http`) extends that reach to the network; see
[Remote access](#remote-access-chatgpt--other-remote-mcp-clients) for the extra precautions it demands.
Two things are on by default to reduce the blast radius: **secret redaction** scrubs high-confidence credentials
from all tool output and saved memory (`WINX_NO_REDACT=1` to disable), and the PTY shell's whole process group is
killed on teardown so background jobs it spawned don't leak.
If you want a tighter leash:
- `architect` mode disables writes and most commands;
- `code_writer` mode lets you allowlist commands and write globs;
- `WINX_SANDBOX=1` enables an opt-in Landlock filesystem sandbox (Linux): writes are confined to the workspace
plus `/tmp`, and the home directory is unreadable, so a manipulated agent can't read `~/.ssh`/`~/.aws` or modify
files outside the project.
[SECURITY.md](SECURITY.md) has the disclosure process and threat model.
## License
MIT - Gabriel Maia ([@gabrielmaialva33](https://github.com/gabrielmaialva33))