Skip to content
Conduit

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.

HashemKhalifa/desktop-relaycommunitylistedunreviewed

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

Desktop 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:

  1. Run bin/dc-relayctl mint --name chatgpt --kind path-only --tools all on the Mac. Save the returned secret privately; it is displayed only when minted.
  2. 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>/mcp and 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.
  3. 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

CommandPurpose
bin/dc-relayctl statusShow daemon and upstream state
bin/dc-relayctl doctorCheck listener ownership and LAN exposure
bin/dc-relayctl dashboardOpen the live local usage page
bin/dc-relayctl dashboard --no-openExport a static HTML report
bin/dc-relayctl logsRead service logs
bin/dc-relayctl auditRead metadata-only audit events
bin/dc-relayctl listList 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 on 127.0.0.1:8789 and 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_value and give_feedback_to_desktop_commander are denied for everyone (they can loosen guardrails remotely). get_recent_tool_calls needs --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

License

MIT

↑↓ navigate openesc close