Desktop agent engineering

Local MCP security: why loopback is not enough

Binding an agent API to localhost removes the network perimeter; it does not create an authorization model. A desktop integration still has to defend its credential bootstrap, browser-origin boundary, long-running work and user-visible side effects.

In short: Treat a local MCP server like a privileged local service. Cadre combines a loopback-only socket with remote-address, Host, Origin, route and bearer-token checks; rotates the port and 256-bit token on every launch; gives long jobs separate capability tokens; and keeps recording visible. That narrows risk, but it does not make a cloud-backed client local or remove the current caller-selected export-path risk.

Start with the threat model, not with localhost

A loopback bind means another machine cannot open the service directly. The same Mac can still run browsers, extensions, local processes and multiple MCP clients. Some of those processes may be compromised; others may simply hold more authority than one task should receive.

For a desktop creative tool, the sensitive actions are concrete: read a transcript or frame, start a screen recording, change a project, import a private file or write an export. The useful question is therefore not “is the port public?” but “which process can cause which effect, for how long, with which visible signal?”

BoundaryFailure to considerCadre v1 control
NetworkAnother host reaches the serviceBind only to 127.0.0.1; reject a non-loopback peer
BrowserA page reaches localhost through DNS rebinding or a hostile originValidate Host and, when present, Origin as loopback
Local processA process guesses or reuses the application credentialFresh 32-byte bearer token per boot, stored in an owner-only file
Shared clientOne bearer-authorized client polls or cancels another client's workIndependent jobId plus secret jobToken capability
HumanAn agent starts a capture without the user noticingCreate the normal floating controls; default the countdown on; keep screen cues and macOS TCC
Cloud clientLocal media context crosses into a model providerState that the client is a separate data boundary and let its account policy apply

Gate every request before the protocol sees it

Cadre's v1 server does not rely on one check. It applies a fixed ingress sequence before handing JSON to the MCP transport:

  1. The TCP peer address must be IPv4 or IPv6 loopback.
  2. The HTTP Host header must name 127.0.0.1, localhost or IPv6 loopback. Missing and malformed values fail closed.
  3. A native MCP client may omit Origin. If an origin is present, it must use HTTP or HTTPS and name a loopback host.
  4. The only accepted route is POST /mcp.
  5. Every request must carry the current bearer token.
  6. The request must be JSON and remain within the one-megabyte body limit; header size and counts are bounded too.

This ordering matters. Authentication alone does not stop a hostile page from probing localhost, and a correct Host string alone says nothing about which process possesses the credential.

CORS is not the control. Cadre does not make the local service broadly callable by adding permissive cross-origin headers. It validates the incoming origin when one exists and still requires the bearer token on every request.

Use a rotating bootstrap credential, not a permanent localhost secret

On every launch, Cadre generates 32 random bytes and encodes them as a URL-safe bearer token. The HTTP server listens on an ephemeral port, so the port changes with the token. Both values are written to the current user's application-data directory in agent-api.json.

The write requests mode 0600 and follows it with an explicit chmod(0600) attempt so a previously looser file is corrected during a normal write. The token is plaintext and the permission does not isolate it from every process running as the same user. The record also contains the process ID and process start time so a shipped bridge can reject a stale file after a crash or PID reuse. Cadre's shutdown routine deletes the connection file before closing the server, while a crash or force-quit may leave a stale file behind.

Cadre hashes the supplied and expected values with SHA-256, then uses timingSafeEqual on the two fixed-width digests. This avoids a direct variable-length buffer comparison; it is not presented here as proof that the complete HTTP authentication routine is cryptographically constant-time. A missing or incorrect credential receives the same 401 response.

What rotation buys: a copied configuration naturally expires at the next app launch. It is not a sandbox. Any process already running as the same user may have other ways to read user-owned files or control the desktop, so local authorization must be evaluated inside the operating system's process and account model.

Give long-running work its own capability

A desktop task such as importing video or generating captions can outlive a normal request timeout. Keeping the original HTTP request open is fragile; returning only a guessable job identifier lets any bearer-authorized client enumerate or interfere with work.

Cadre returns a random UUID and a separate 32-byte jobToken immediately for imports, caption generation, transcription and model downloads. Status, result and cancellation calls must present the exact pair. An unknown ID and a wrong token intentionally return the same NOT_FOUND error, so the API does not become a job-existence oracle.

The registry is bounded and boot-local:

  • no more than four jobs may be active;
  • conflicting work uses an exclusive key and fails as busy;
  • the default runtime ceiling is two hours;
  • progress is limited to 8KiB and a retained result to 1MiB;
  • completed results expire after 15 minutes, with at most 32 terminal records retained; and
  • shutdown aborts active runners and destroys every job secret.

This is capability security at a deliberately small scale: possession of the app-wide bearer token opens the API, while possession of the job pair grants access to one background operation.

Security includes visible product behaviour

An agent start follows the same main-process recording path as the app's Record button. Cadre creates the floating recording-controls window for the take. The countdown defaults on but the caller can turn it off; for screen and area capture, the target highlight and other-display dimming are conditional, best-effort cues rather than hard security gates. macOS Screen Recording permission still governs desktop capture, and the automation path cannot grant it.

The same principle applies to editing and export. Agent mutations use the live editor actions, validation, undo history and autosave used by a person. Export uses the existing main-process licence gate. A client can request the action, but it does not receive a second, less restricted implementation.

Visibility has a limit worth stating: there is no server-side recording duration. If an agent stops responding after it starts capture, recording continues until the person stops it or Cadre's disk monitor reaches its critical threshold. Removing silent capture is useful; it is not the same as making every workflow self-terminating.

Local transport does not make a cloud-backed client local

The connection from Cadre to the MCP client stays on the Mac. Once the client requests a transcript, interaction summary, OCR result, video frame, project name, local path, timeline state or licence status, that result enters the client's process. A cloud-backed client may then send it to its model provider under that provider's account settings and data policy.

This distinction should be part of the product copy and permission flow, not buried in a protocol document. “Local MCP server” describes the transport between the desktop app and the client. It does not describe what the client does next.

For sensitive recordings, begin with structured, bounded context and request frames only when text is insufficient. Cadre's screen-recording privacy checklist treats the connected assistant as a separate disclosure decision.

Document the residual risk that remains

Cadre v1 lets export_video write to an absolute .mp4 or .mov path selected by the caller. Validation rejects relative paths, NUL bytes, parent traversal, leading-dash filenames, excessive length and destinations inside the open project's source-recording directory. An active Pro licence is also required.

Those checks protect the media source and command-line boundary; they are not a general write allow-list. The current API can still target another caller-selected location available to the user's process. The public specification names this explicitly and recommends treating the agent like any other local process that writes files on the user's behalf.

A stronger future design could bind export to directories chosen by the person, issue a narrow write capability for that selection and expire it after the job. Until then, the honest security statement includes the limitation.

A reusable checklist for local desktop MCP servers

  1. Bind narrowly: use an explicit loopback address, not an unspecified interface.
  2. Validate the peer, host and browser origin: localhost is a routing fact, not a request identity.
  3. Authenticate every call: mint a high-entropy secret and compare it without early-exit timing differences.
  4. Rotate discovery state: pair an ephemeral port with a boot-scoped credential and handle crash-stale files.
  5. Protect the bootstrap file: create and re-check owner-only permissions; never log the token.
  6. Bound ingress: restrict methods, routes, content type, body size, header size and time spent reading a request.
  7. Split long-job authority: return a secret job capability, avoid existence oracles and expire retained results.
  8. Reuse human gates: permissions, licence checks, validation, undo and visible controls should not fork for agents.
  9. Explain the next data hop: distinguish the local client connection from the model provider it may call.
  10. Publish known limits: residual file, process and side-effect authority belongs in the contract.

Download the complete Markdown checklist to review or adapt these controls for another desktop integration. It is a threat-model prompt, not a security certification, and may be copied without attribution.

Run a redacted check against a live Cadre instance

The dependency-free Cadre local MCP security verifier checks the owner-only connection record, process and loopback listener, hostile Host and Origin rejection, unauthenticated rejection, version consistency and the current unique 57-tool surface. It sends only initialize, notifications/initialized and tools/list.

node cadre-local-mcp-security-verifier.mjs
node cadre-local-mcp-security-verifier.mjs --json

The report omits the bearer token, connection-file path, port and process id. It is a diagnostic snapshot, not a penetration test or security certification.

What the published evidence proves

This article describes the Cadre 1.0.0-rc.29 source and v1 public contract as inspected on 26 August 2026. The Agent API security specification and 57-tool reference are the canonical details.

Cadre also publishes a version-pinned rc.20 compatibility record with artifact hashes, sanitized raw JSON and a frozen collector. That record proves an authenticated MCP initialization, ordered discovery of 57 advertised tools and one benign get_app_state call. It does not prove that all tools ran, that every security control resisted an attack, or that recording, editing, captions and export succeeded. Compatibility evidence and a security assessment are different claims.

Inspect the contract, not just the claim

Cadre publishes its Agent API security posture, complete tool constraints and reproducible compatibility evidence for technical review.