Skip to content
Conduit

mcp server

CitrixMCP

MCP server that drives a Windows PowerShell session inside a Citrix/RDP window over the synced clipboard — run commands, control the desktop, transfer files, no SSH/WinRM needed.

FrancoisChastel/CitrixMCPcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 21, 2026

🖥️ CitrixMCP

Drive a Windows PowerShell session inside a Citrix / RDP window — from Claude, over the clipboard.

Run commands, control the desktop, capture screenshots, and move files in and out of a locked-down remote session that has no SSH and no WinRM — using the one channel that always crosses the gap: the synced clipboard.

CI License: MIT Node MCP


Why this exists

Some Windows machines are only reachable through a double hop — a Citrix Workspace client that publishes an RDP window, which in turn logs into the box:

 Your Mac  ──Citrix Workspace──▶  published mstsc.exe  ──RDP──▶  Windows box + PowerShell

There is no direct network path (no SSH, no WinRM) to that final machine. The one thing that reliably travels across all three boundaries is the clipboard, which Citrix keeps synchronized in both directions. CitrixMCP turns that shared clipboard cell into a reliable, ordered command-and-file channel, and exposes it to Claude (or any MCP client) as a clean set of tools.

Architecture

flowchart LR
    A["Claude session<br/>(MCP client)"] -->|stdio| B["CitrixMCP server<br/>Node · macOS"]
    B <-->|pbcopy / pbpaste| CB1["macOS clipboard"]
    CB1 <-->|Citrix clipboard sync| CB2["Windows clipboard"]
    CB2 <-->|Get / Set-Clipboard| H["rdt-agent helper<br/>PowerShell or Python"]
    H -->|runs| PS["PowerShell · desktop · files"]

    classDef mac fill:#0b7285,stroke:#083f4d,color:#fff
    classDef win fill:#5f3dc4,stroke:#3b2593,color:#fff
    class A,B,CB1 mac
    class CB2,H,PS win

The link runs a classic stop-and-wait protocol over that single shared cell: each message is a full clipboard write of RDT1|<base64(json)>, with sequence numbers for ordering and de-duplication, automatic retransmit if the user clobbers the clipboard mid-exchange, and chunking for large payloads.

Features

  • 🧑‍💻 Full PowerShell control — persistent working directory, real output.
  • 🪟 Desktop & GUI control — list/focus windows, move & click the mouse, type text, send key combos.
  • 📸 Screenshots — capture the remote desktop as PNG and see it inline.
  • 📦 File transfer both ways — binary-safe, chunked, hash-verified.
  • 🔌 No SSH / WinRM / agents on the network — clipboard only.
  • 🛡️ Safe, auditable helper — no network, no persistence, prints every command.
  • 🔁 Reconnect-friendly — survives Citrix disconnect/reconnect and Claude restarts.
  • 🐍 Two interchangeable helpers — PowerShell or stdlib-only Python (for SRP-locked boxes).

Quick start

1. Register the MCP server (on the Mac)

Zero-install, straight from the repo (npm clones and builds it for you):

claude mcp add citrix -s user -- npx -y github:FrancoisChastel/CitrixMCP
git clone git@github.com:FrancoisChastel/CitrixMCP.git && cd CitrixMCP
npm install          # installs deps and builds dist/ via the prepare script
claude mcp add citrix -s user -- node "$PWD/dist/index.js"

Claude Desktop (claude_desktop_config.json) or any MCP client:

{
  "mcpServers": {
    "citrix": {
      "command": "npx",
      "args": ["-y", "github:FrancoisChastel/CitrixMCP"]
    }
  }
}

-s user registers it for all your projects/sessions. Add an "env" block for any configuration overrides.

2. Start the helper (inside the Citrix/RDP Windows session)

Pick whichever your locked-down box allows — both speak the same protocol, so the server works with either.

PowerShell (windows/rdt-agent.ps1):

.\rdt-agent.ps1 -DryRun   # safe first run: prints commands, executes nothing
.\rdt-agent.ps1           # go live

“cannot be loaded … blocked by software restriction policies”? That blocks the .ps1 file, not PowerShell. Check $ExecutionContext.SessionState.LanguageMode:

  • FullLanguage → run it interactively: Get-Content .\rdt-agent.ps1 -Raw | iex
  • ConstrainedLanguage → use the Python helper below.

Python (windows/rdt_agent.py) — standard library only, any Python 3.6+:

python rdt_agent.py --dry-run   # or:  py rdt_agent.py --dry-run
python rdt_agent.py

Leave the helper running. Stop it anytime with Ctrl+C.

3. Use it

Ask Claude to rdt_ping (confirms the round-trip), then rdt_run a command. That’s it. 🎉

rdt_ping reports the server version, protocol version, and the helper's version + gzip support, so you can confirm both ends match. The helpers also print their version in the startup banner and support --version (Python) / -Version (PowerShell).

Tools

ToolWhat it does
rdt_pingConfirm the helper is alive; returns host / user / PowerShell version.
rdt_runRun a PowerShell command/script; working directory persists between calls. The universal tool.
rdt_launchStart-Process a program (powershell.exe, notepad.exe, a path, a document); returns PID.
rdt_list_windowsList visible top-level windows as JSON [{pid, process, title}].
rdt_focusBring a window to the foreground by PID or title substring.
rdt_send_keysSend key combos (.NET SendKeys syntax, e.g. ^c, {F5}), optionally activating a window first.
rdt_typeType arbitrary literal text (special chars auto-escaped; newlines press Enter).
rdt_mouseMove the cursor to (x, y) and optionally click (left/right/middle · click/double).
rdt_screenshotCapture the remote desktop as PNG and return it as an image.
rdt_processesList top processes by memory as JSON [{pid, name, ws_mb, cpu}].
rdt_uploadSend a local file or inline text into a file on the Windows box (binary-safe, chunked).
rdt_downloadPull a file off the Windows box to a local path (binary-safe, chunked).

A GUI-driving loop: rdt_screenshot to see → rdt_list_windows / rdt_focus to target → rdt_mouse / rdt_type / rdt_send_keys to act → screenshot to confirm.

How it works

sequenceDiagram
    participant S as CitrixMCP (Mac)
    participant C as Clipboard (Citrix-synced)
    participant H as Helper (Windows)
    S->>C: frame n — req: run "Get-Date"
    C-->>H: synced by Citrix
    H->>H: execute in the PowerShell session
    H->>C: frame n+1 — reply: output (chunked if large)
    C-->>S: synced by Citrix
    S->>S: reassemble → return to Claude

One operation is in flight at a time (there is a single clipboard cell), each frame is acknowledged before the next, and a frame is retransmitted if the peer goes quiet — so a stray copy/paste during a transfer is recovered, not fatal. The src/protocol.ts file is the single source of truth for the frame format; both helpers implement it identically (verified byte-for-byte in CI).

Configuration

Environment variables on the server (Mac) side, all optional:

VariableDefaultMeaning
RDT_POLL_INTERVAL_MS250Clipboard poll interval.
RDT_RETRANSMIT_MS2500Re-assert a frame if the peer goes quiet this long.
RDT_FRAME_TIMEOUT_MS60000Give up on one frame round-trip.
RDT_EXEC_TIMEOUT_MS120000Default per-command wait.
RDT_CHUNK_BYTES262144Payload bytes per frame before base64.
RDT_RESTORE_CLIPBOARDtrueRestore the user’s clipboard after each op.
RDT_DEBUGfalseVerbose frame logging to stderr.

Helper flags mirror these: -PollMs, -ChunkBytes, -DefaultTimeoutMs, -DryRun, -DenyRegex, -Quiet (PowerShell) / --dry-run, --deny-regex, … (Python).

Safety

The Windows helper runs on what may be a sensitive machine, so it is deliberately conservative:

  • One readable file, no obfuscation — read it top to bottom before running.
  • No network. It never opens a socket; the clipboard is the only channel.
  • No persistence. Runs in your console only; Ctrl+C stops it. No service, scheduled task, registry key, or startup entry.
  • Does nothing on its own. It only runs the commands you send — the same ones you could type yourself. Runs as you, no elevation.
  • Transparent. Every command is printed before it executes.
  • Opt-in guardrails. -DryRun / --dry-run echoes without executing; -DenyRegex / --deny-regex refuses matching commands.

Only reacts to clipboard values beginning with the RDT1| marker — normal copy/paste is ignored.

Testing

Offline (no Windows, no Citrix):

npm test                         # build + relay self-test + builder unit tests
python3 scripts/selftest_py.py   # Python responder self-test

Live (helper running, clipboard synced) — drives every tool through the real MCP interface:

node scripts/test-all.mjs                 # safe: never sends input to the desktop
node scripts/test-all.mjs --interactive   # also tests keyboard input via Notepad

Focused probes: scripts/ping-client.mjs (ping + one command) and scripts/transfer-test.mjs (700 KB binary round-trip with a Windows-side hash check).

Large files

File transfer (rdt_upload / rdt_download) is built for big files, both ways:

  • Gzip in transit, on by default. Compressible data (logs, dumps, text, most office files) shrinks a lot before it hits the clipboard. The server only compresses when the helper advertises support (via ping), so an older helper never receives bytes it can't decompress — it transparently falls back to raw.
  • Streamed to and from disk. Neither side loads the whole file into memory, so multi-GB files won't OOM the server or the helper.
  • Integrity is easy to verify — hash on the Windows side with rdt_run "(Get-FileHash -Algorithm SHA256 -LiteralPath 'C:\\path').Hash" and compare with a local shasum -a 256.

Throughput is still bounded by clipboard round-trip latency (see below), so for very large or already-compressed payloads, prefer a faster channel (a network share both machines can reach) when one is available. Pass compress: false for already-compressed files (zip, jpg, mp4).

Limitations

  • Control channel, not a bulk pipe — ops are serialized; throughput is bounded by clipboard sync latency × round-trips. Compression widens the effective pipe for compressible data, but incompressible multi-GB transfers are still slow.
  • Don’t copy/paste while an op runs — it transiently owns the clipboard.
  • Manual bootstrap — you start the helper once by hand (by design; no auto-start on a sensitive box). After that, the agent can launch more windows/apps itself.

Contributing

Issues and PRs welcome. Please run npm test (and, if you touched the protocol or the Python helper, python3 scripts/selftest_py.py) before opening a PR — CI runs the same checks. Keep the two helpers byte-compatible with src/protocol.ts.

License

MIT © Francois Chastel

↑↓ navigate openesc close