mcp server
desktop-relay
Self-hosted MCP relay for ChatGPT to read files and run commands on your machine, with per-client authentication, tool permissions, audit logs, and a live dashboard.
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, 2026Desktop Relay
Run Desktop Commander MCP on your own machine behind your own authenticated HTTPS endpoint, with no monthly tool-call quota.
Install · ChatGPT setup · Recovery · Releases
The supported installation runs on a Mac that is awake, online, and logged in. This is access to your existing OS account: a client with command-execution permission can run commands with that account's privileges. Use it with trusted clients. Each installation serves one shared machine.
Dashboard preview using sample data.
Architecture
remote MCP client
-> https://<your-edge>/mcp TLS at the edge
-> tunnel agent on your Mac Cloudflare named tunnel
-> 127.0.0.1:8788 src/daemon.ts auth + limits + audit
|-- one SDK Server + transport per client session (session.ts)
|-- policy-checked tools and advertised UI resources (session.ts)
|-- one SDK Client + one desktop-commander child (upstream.ts)
-> desktop-commander (stdio child) the OSS MCP server
The relay terminates each downstream MCP connection: every remote client gets its own protocol session (capabilities, cancellation, progress ownership), while all sessions share one long-lived desktop-commander child — the "one device" semantic the vendor relay provides. Only the relay's own SDK Client ever initializes the child.
Node 24 runs the TypeScript directly (native type stripping). Runtime deps:
@modelcontextprotocol/sdk@1.31.0 + @wonderwhy-er/desktop-commander@0.2.52, both
exact-pinned.
Install with Cloudflare
Requires macOS, Node 24 or newer, pnpm 10, cloudflared, and a domain managed by Cloudflare.
Replace mcp.example.com with a hostname you control.
Install pnpm if needed with npm install --global pnpm@10.34.5. The repository
pins its package-manager version in package.json.
git clone git@github.com:HashemKhalifa/desktop-relay.git
cd desktop-relay
pnpm install --frozen-lockfile
On the Mac, log in to Cloudflare and create the tunnel and DNS route once:
cloudflared tunnel login
cloudflared tunnel create desktop-relay
cloudflared tunnel route dns desktop-relay mcp.example.com
Put the tunnel UUID returned by create into ~/.cloudflared/config.yml:
tunnel: <UUID>
credentials-file: /Users/<mac-user>/.cloudflared/<UUID>.json
ingress:
- hostname: mcp.example.com
service: http://127.0.0.1:8788
- service: http_status:404
Keep the certificate, tunnel credentials, and config under ~/.cloudflared, outside
the repo. Then validate and install the launch agents:
cloudflared tunnel ingress validate
./install.sh --edge cloudflare --domain mcp.example.com
scripts/verify.sh https://mcp.example.com
bin/dc-relayctl doctor
The daemon and tunnel run as macOS LaunchAgents after login. Both restart on failure;
the daemon listens only on 127.0.0.1:8788. mcp.example.com is an example hostname.
Using it
Bearer clients (Codex, Claude connectors — header-capable):
url: https://<edge>/mcp
header: Authorization: Bearer <secret from mint>
Header-less clients get a path-only credential — the URL path itself is the secret:
bin/dc-relayctl mint --name phone --kind path-only --tools all
# -> https://<edge>/<pathToken>/mcp
ChatGPT
Connect your endpoint as a custom MCP app named Desktop Relay. Availability, developer-mode permissions, and menu labels vary by account and workspace; check OpenAI's setup guide for the current requirements.
For a client that cannot send a custom bearer header:
- Run
bin/dc-relayctl mint --name chatgpt --kind path-only --tools allon the Mac. Save the returned secret privately; it is displayed only when minted. - Open the app-creation form, typically Settings → Apps → Create. Interfaces
with a Plugins menu use Customize → Plugins → Add → Create MCP App.
Name it Desktop Relay. Set Server URL to
https://mcp.example.com/<pathToken>/mcpand Authentication to No authentication. The path token authenticates the request; treat the complete URL as a secret. Confirm the access warning, create the app, and connect it. - Start a fresh chat, select Desktop Relay from +, and try a harmless tool call. Keep the paid app until this real-client check succeeds.
The relay supports static bearer and path-only credentials; it does not implement an OAuth authorization server. Never paste the token into a chat, issue, log, or repository file.
The relay exposes Desktop Commander's structured tool arguments directly; a device
ID is unnecessary because this endpoint targets one Mac. Compact descriptions in
src/tool-descriptions.ts cover the pinned upstream tools; their argument schemas
and risk annotations remain intact. Automatic renderer metadata is removed;
explicit relay previews advertise their own viewer. Unrecognized tools retain their
upstream description. ChatGPT still controls
action approvals, and its saved tool definitions may need refreshing after an update.
To refresh them, use the app's Manage → Refresh tools control where available. In the Plugins interface, open Plugins → Desktop Relay → More actions → Manage. Workspace app updates may require administrator review; follow OpenAI's guide. Reload the app details page if it still shows old descriptions, then start a new chat. This updates the existing connection without replacing its credential. The plugin's displayed version and the relay's source version are separate; the installed tool list is the useful check.
For connection errors, follow Connection recovery.
After a Mac or daemon restart
After logging in to macOS, the launch agents start the daemon and Cloudflare tunnel. The existing ChatGPT app and path credential remain configured; select Desktop Relay in a new chat and use it normally. An MCP session interrupted by a restart must initialize again. If a tool call was interrupted, its outcome may be unknown: check its effect before deciding whether to issue a new call.
If ChatGPT cannot connect, check the live service and tunnel:
bin/dc-relayctl status
bin/dc-relayctl doctor
cloudflared tunnel info desktop-relay
scripts/verify.sh https://mcp.example.com
doctor should show 127.0.0.1:8788 listening and no LAN listener. If the daemon
is stopped, run bin/dc-relayctl restart; if the Mac is asleep, offline, or logged
out, the public endpoint may be unavailable until it wakes and the user logs in.
Wait for status to report upstream: running after a restart. The control socket
can answer before the MCP listener is ready. Zero MCP sessions is normal when
no client has initialized a session. It is a count of open protocol sessions,
not a server-health indicator. Starting a new ChatGPT request establishes a session;
you do not need to recreate the app or mint another credential after a restart.
Idle MCP sessions expire after 15 minutes. At the 300-session default limit, the relay can reclaim the oldest session idle for at least one minute; in-flight requests are preserved. A client whose session expires must initialize again. This cleanup does not restart Desktop Commander or rerun commands.
Local usage dashboard
Run bin/dc-relayctl dashboard on the Mac to open the live local dashboard.
The daemon serves it at http://127.0.0.1:8789 (configurable with dashboardPort).
It loads usage and daemon status immediately when opened or reloaded, refreshes
when you return to the tab, and polls every ten seconds while visible. It starts
with the daemon after login. The CLI opens it with
a temporary URL fragment that the page exchanges for an HttpOnly cookie and removes
from the address bar. The key stays in ~/.config/desktop-relay/dashboard.key.
It shows authenticated MCP request counts, tool calls, and health checks for today,
this month, the last 14 days, and the last 12 months. --no-open exports a standalone
snapshot to
~/.config/desktop-relay/dashboard.html.
The report is generated from the metadata-only audit file and its previous rotated file. Tool-call history already in the audit is included; request counts begin with the request-audit event added alongside this dashboard. Older HTTP requests cannot be backfilled, and the relay cannot count ChatGPT tokens or model costs. The report stays on the Mac and is not served by the public tunnel.
The overview includes a 14-day activity chart and daily/monthly ledgers. To change
the palette, spacing, or typography, edit src/dashboard.css; the :root variables
define the colors. Styles are scoped under .relay and embedded in the generated
HTML, so the report remains a single file with no build step or external assets.
Result bytes measure JSON traffic, including retained-result page reads, not model
tokens.
File previews in ChatGPT
Normal reads, directory listings, config reads, writes, and edits return data to
ChatGPT without opening automatic upstream widgets. Old widgets' origin: "ui"
calls are stopped before they can reread files or config on chat reload.
Ask explicitly to open or preview a local file to get a compact Desktop Relay
card with the app icon through preview_relay_file. Click Open file to browse
local text and source files. The viewer reads up to 200 lines at a time and includes
line navigation, search within the current range,
wrapping, copy, download, and an expanded view where the host supports it.
Refresh rereads the displayed range and reports changes. Browsing reads the current file on the Mac; it can differ from an earlier tool result. Copy and download contain the displayed range, not the entire file. Media is still available in the original tool result.
Preview reads use an authenticated app-only tool, with file contents in
component-only metadata. Expanding a preview does not send its file text as a
model-facing tool result. The ordinary read_file result still supplies text to
the model. The dashboard distinguishes model dispatches, preview reads, and
component payload bytes; older calls remain unclassified.
After updating and restarting the relay, refresh Desktop Relay's tool definitions in ChatGPT, then verify an ordinary read and an explicit preview in a fresh chat. Reload the chat to check that the answer remains visible without a viewer opening. Existing upstream cards may report that automatic previews are disabled; request a new explicit preview to browse a file. The relay advertises its icon through MCP server metadata; whether it replaces the connector's icon in ChatGPT's menus depends on the host.
See file viewer design and verification.
Large tool results and context
Plain-text results above 16 KiB return a short preview and a result ID. Use
read_relay_result with that ID, contentIndex (default 0), and offset (default 0)
to retrieve up to 12 KiB of exact UTF-8 text per page. Follow nextOffset until null,
then increment contentIndex to read any remaining text blocks. This reads the
original stored output; it never reruns the command.
Results are scoped to the original principal and source-tool grant, including across new MCP sessions. They expire after one hour, may be evicted earlier under memory pressure, and disappear on daemon restart. The cache is 64 MiB total with a 32 MiB per-principal limit. Revocation clears that principal's retained results. An unavailable result does not establish that the original command failed.
Small results, media, structured/schema-bound results, annotated text, widget-origin calls, and results larger than the per-principal cache limit preserve their native responses. These exceptions can still be large. Audit events record byte counts and whether a result was offloaded, without recording its contents.
After updating, refresh Desktop Relay's tool definitions in ChatGPT so it discovers
read_relay_result. This reduces large responses added to future turns; it does not
shrink existing chat history or change ChatGPT's conversation limits.
Run node scripts/verify-context.ts https://mcp.example.com to check exact output
recovery, principal isolation, revocation, and one-time command execution against
the public endpoint. Use your own hostname on another installation.
Update an existing installation
Wait for active tool calls and terminal jobs to finish before restarting. From the repository directory:
git pull --ff-only
pnpm install --frozen-lockfile
bin/dc-relayctl restart
bin/dc-relayctl doctor
The restart loads the new daemon code and dashboard assets. It also clears MCP sessions, terminal output tracking, and retained results; save needed output first. Your credentials and Cloudflare setup persist. Refresh the app's tool definitions in ChatGPT after tool changes, then start a new chat with Desktop Relay selected. You do not need to run the installer again for a normal update.
Versioned releases
Release notes identify the
source commit, changes, verification, and known limitations for each version.
CHANGELOG.md summarizes changes in the checkout. Draft releases are
for review; use a published tag when you want a fixed version instead of main.
To install a published version, run git fetch --tags, then
git switch --detach <release-tag> before pnpm install --frozen-lockfile.
Replace <release-tag> with the version you selected. Preserve local changes
before switching versions. Updating or rolling back the running service still
requires a restart.
Releases contain source and the checked-in viewer bundle. There is no npm package, auto-updater, or bundled credential. See Preparing a release for the verification and publication checklist.
Manage the relay
| Command | Purpose |
|---|---|
bin/dc-relayctl status | Show daemon and upstream state |
bin/dc-relayctl doctor | Check listener ownership and LAN exposure |
bin/dc-relayctl dashboard | Open the live local usage page |
bin/dc-relayctl dashboard --no-open | Export a static HTML report |
bin/dc-relayctl logs | Read service logs |
bin/dc-relayctl audit | Read metadata-only audit events |
bin/dc-relayctl list | List principals and credential metadata |
bin/dc-relayctl rotate --principal-id prin_… | Rotate with 24-hour grace |
bin/dc-relayctl revoke --principal-id prin_… | Revoke credentials and close sessions |
Request rate limits
Limits count HTTP requests, not tool calls: initialization, notifications, calls, and result-page reads each use allowance. Authenticated unsupported MCP GET probes return 405 without consuming it. New credentials default to 60 requests/minute. For an active ChatGPT account, use 300 requests/minute:
bin/dc-relayctl set-rate --principal-id prin_… --rate 300
This applies immediately and preserves the credential and sessions. The burst
allowance is half the per-minute rate, rounded up, with a maximum of 150 requests.
HTTP 429 includes Retry-After in seconds. The relay never automatically replays a
tool call; check the outcome of any interrupted call before issuing another.
Security model
Report suspected vulnerabilities through the private channel in SECURITY.md.
- The MCP listener binds to
127.0.0.1:8788; the tunnel forwards to it and the daemon authenticates requests. The separately authenticated dashboard listens on127.0.0.1:8789and is not exposed by the tunnel. MCP failures:404(auth),403(Host/Origin),413/408(body limits),429(rate),503(upstream down/overloaded). - Per-principal grants validated against the pinned binary's
tools/list.set_config_valueandgive_feedback_to_desktop_commanderare denied for everyone (they can loosen guardrails remotely).get_recent_tool_callsneeds--allow-shared-history— it exposes other sessions' arguments. - Audit is metadata-only (principal, session, tool, outcome) — never arguments or
output bodies.
~/.config/desktop-relay/audit.jsonl. - Honest scope: host mode means the bearer grants the privileges of the OS account
running the daemon. Trusted clients of one operator — not isolation. Rotate/revoke
via the daemon's control socket (
~/.config/desktop-relay/control.sock, mode 0600); revoking closes live sessions.
Development
See Contributing for the project layout, local checks, and dependency-update workflow.
Docs
- DESIGN.md — architecture, threat model, why protocol ownership
- SYNTHESIS.md — architecture decisions and rejected alternatives
- ACCEPTANCE.md — the full verification contract
- Context and dashboard design — response limits, alternatives, and evidence
- vps/README.md — self-sovereign SSH edge
License
MIT