05 AGENT BRIDGE // MCP surface & socket routing SRC scripts/connector-mcp.py — 754 lines
05.A

Semantic tools, not a shell

FastMCP("Connector")

Handing the console to a model

It would have been trivial to expose one tool called run_command and be done. The MCP server instead exposes thirty-three named operations whose parameters are typed, whose device references are resolved before anything executes, and whose failures come back as structured blockers with suggested next actions. The design assumption is that an agent works better when the surface tells it what it is allowed to want.

The server is a single Python file built on mcp.server.fastmcp. It imports the same ConnectorControlClient the CLI uses, and holds two instances of it: one bound to the resolved default socket, one bound explicitly to the desktop app's socket. Everything interesting follows from that pair.

33@mcp.tool()typed, documented operations
11@mcp.resourceconnector:// URI space
2@mcp.prompt()connect_and_verify, repair_host_readiness
1.27.0pinned MCP SDKrun-connector-mcp.sh

"Use Connector semantic tools first. Select device {device_id}, inspect the device state, connect, then capture a screenshot to verify the UI state."

@mcp.prompt() connect_and_verify — the entire prompt, verbatim
05.B

Launching it

scripts/run-connector-mcp.sh

The wrapper does two jobs before exec: it guarantees a daemon, and it pins the SDK.

run-connector-mcp.sh
# unless CONNECTOR_MCP_USE_APP_SOCKET=1
CONNECTOR_CONTROL_SOCKET="$(python3 ensure-connectord.py --print-socket)"
export CONNECTOR_CONTROL_SOCKET

exec "$UV_BIN" run --with "mcp==1.27.0" \
     python3 scripts/connector-mcp.py "$@"

The uv binary is searched for at ~/.local/bin/uv, then /opt/homebrew/bin/uv, then on PATH — so the wrapper works from a systemd unit with a minimal environment.

Transport flags

connector-mcp.py argument parser
FlagDefault
--transportstdio · also sse, streamable-http
--host127.0.0.1
--port8765
--path/mcp
--mount-path/

The shipped systemd unit overrides the port to 8876 via CONNECTOR_MCP_PORT, and binds to loopback only.

05.C

Which socket answers

routed_rpc() · visual_rpc() · session_rpc()

This is the most consequential twenty lines in the file. The daemon is the default because it is always there. But a stream launched by connectord is invisible to the desktop app's menubar and local session controls — so anything visual has to prefer the app when the app is open. The routing is explicit, and it is commented in the source with exactly that reasoning.

MCP socket routing decision tree A decision tree taking an RPC method name and dispatching it to either the daemon socket or the desktop app socket, with a separate branch for session methods that first checks whether either side has an active session. routed_rpc(method, params) every tool and resource enters here VISUAL — visual_rpc() devices.connect ui.* host.set_enabled · set_credentials host.approve_pair · revoke_client host.set_default_monitor SESSION — session_rpc() any method starting with session. follows ownership, not preference — the socket that actually holds the live session wins state queries are answered without side effects EVERYTHING ELSE devices.list · devices.get ssh.prepare · ssh.exec settings.* · host.state app.state is special-cased to the app when open app_socket_available() app.ping on the app socket, 0.75 s and ping["mode"] != "daemon" a daemon answering on the app path does not count SESSION LADDER — IN ORDER 1 · app open AND app session active|connecting → app socket 2 · method == session.state → return the daemon's state as read 3 · daemon session active|connecting → daemon socket 4 · fall back to the app if open, else the daemon client = ConnectorControlClient() default resolved socket app_client = ConnectorControlClient( default_app_socket_path()) DESKTOP APP SOCKET /tmp/connector-control-<sha1(home)[0:12]>/control.sock answers all 43 methods including ui.* and host.set_* the stream it starts is visible in the menubar and overlay requires a logged-in desktop session DAEMON SOCKET $XDG_RUNTIME_DIR/connector/daemon.sock 34 methods · discovery, settings, SSH, host start can still launch connectorstream when a desktop exists survives logout; started by ensure-connectord.py or systemd
Figure 05.1 — routed_rpc, the whole decision Note the guard inside app_socket_available(): it is not enough for something to answer on the app socket path. It must answer and not identify itself as a daemon, because a systemd unit that sets CONNECTOR_CONTROL_SOCKET to the daemon path would otherwise fool the check.
05.D

The tool surface

all 33, grouped by what they touch
Every @mcp.tool() in scripts/connector-mcp.py, with the RPC method it routes to
ToolRoutes viaUnderlying method
connector_get_contextreads CONNECTOR_MCP_CONTEXT.md / .json
connector_rpcrouted_rpcany method, raw JSON params
connector_list_devicesrpcdevices.list
connector_get_devicerpcdevices.get
connector_resolve_devicelocalscoring resolver, ambiguity allowed
connector_diagnose_devicemixeddevices.get + app.state
connector_select_devicerpcdevices.select
connector_connectvisualdevices.connect
connector_pair_devicerpcdevices.pair
connector_rename_devicerpcdevices.rename
connector_set_device_ssh_profilerpcdevices.set_ssh_profile
connector_set_device_desktop_scalerpcdevices.set_desktop_scale
connector_test_connectionrpcdevices.test_connection
connector_start_host_runtimerpcdevices.start_host
connector_disconnect_sessionsessionsession.disconnect
connector_toggle_session_statssessionsession.toggle_stats
connector_toggle_session_fullscreensessionsession.toggle_fullscreen
connector_toggle_session_mousesessionsession.toggle_mouse
connector_release_session_mousesessionsession.release_mouse
connector_switch_session_displaysessionsession.switch_display
connector_get_host_staterpchost.state
connector_set_hosting_enabledvisualhost.set_enabled
connector_set_host_credentialsvisualhost.set_credentials
connector_approve_pairvisualhost.approve_pair
connector_revoke_trusted_clientvisualhost.revoke_client
connector_get_settingsrpcsettings.get
connector_set_settingsrpcsettings.set — six optional fields
connector_get_ui_treevisualui.tree
connector_activate_controlvisualui.activate
connector_set_control_valuevisualui.set_value
connector_capture_screenshotvisualui.screenshot → written to a temp dir
connector_ssh_execrpcssh.exec
connector_prepare_ssh_commandrpcssh.prepare + a shlex-quoted preview

Resources

Eleven read-only connector:// URIs, all of which serialise through the same routing. Two are static files; the rest are live RPC reads.

connector://context
The onboarding packet as markdown, mime text/markdown.
connector://context.json
The same packet, machine-readable.
connector://app/state
app.state
connector://devices
devices.list
connector://devices/{device_id}
devices.get, templated
connector://this-computer
host.state
connector://settings
settings.get
connector://session
session.state
connector://ui/tree
ui.tree
connector://actions/global
actions.global
connector://actions/device/{device_id}
actions.device, templated
05.E

The diagnostic tool

connector_diagnose_device()

Of the thirty-three tools, this one is the argument for the whole design. It does not return a device record. It returns a judgement, assembled from the four independent readiness signals, together with the specific next tool call that would move the situation forward.

Summary strings and the conditions that produce them
Conditionsummary
online + sshReady + hostReachable + clientTrusted"Online, SSH ready, and remote desktop ready."
online + sshReady, but not both host bits"Online and SSH ready; remote desktop still needs host runtime/trust."
online only"Online in Tailscale, but SSH or host readiness is blocked."
not online"Offline"
MCP tool result — connector_diagnose_device("zflow")
{
  "query": "zflow",
  "resolved_device": {
    "id": "n5XYmDgJcq11CNTRL",
    "name": "zflow",
    "ip": "100.64.0.13",
    "online": true,
    "host_reachable": false,
    "ssh_ready": true,
    "ssh_mode": "tailscale",
    "trusted": false
  },
  "summary": "Online and SSH ready; remote desktop still needs host runtime/trust.",
  "can_ssh": true,
  "ssh_target": "[email protected]",
  "can_start_remote_desktop": false,
  "blockers": [
    "Connector host runtime is not reachable."
  ],
  "next_actions": [
    "Run connector_start_host_runtime to start Connector/Sunshine over SSH, then re-test.",
    "Use connector_ssh_exec for remote repair commands."
  ],
  "resolution": {
    "device_id": "n5XYmDgJcq11CNTRL",
    "match": { "score": 1035, "reason": "exact match on zflow" }
  }
}

The score of 1035 is not decorative: 1000 for the exact name match, +25 for sshReady, +10 for online, no bonus for hostReachable because it is false. An agent reading this can see both what matched and how confident the match was.

Composable failure

blockers and next_actions are built in the same pass and stay in sync. If the host is unreachable but SSH is not ready, the tool does not suggest starting the host runtime — because it knows that suggestion would fail.

05.F

Hosted routing

deploy/connector-mcp-http.service

The recommended deployment, straight from CONNECTOR_MCP_CONTEXT.md, is a trusted home control node: a machine that stays on the tailnet, runs the daemon, and publishes the MCP surface on loopback for a local agent runtime to consume. The three deploy files encode exactly that ordering.

control node — hosted bridge
$ systemctl --user enable --now connectord.service connector-mcp-http.service

$ systemctl --user list-dependencies connector-mcp-http.service
connector-mcp-http.service
 └─connectord.service

$ scripts/wait-for-port.py 127.0.0.1 8876 30
$ echo $?
0

# the drop-in that makes a consuming service wait for the bridge
$ cat deploy/ian-brain-connector-order.conf
[Unit]
Wants=connector-mcp-http.service
After=connector-mcp-http.service

[Service]
ExecStartPre=%h/Desktop/realconnector/scripts/wait-for-port.py 127.0.0.1 8876 30
Trust boundary

This bridge has no authentication of its own. It binds 127.0.0.1 and inherits the daemon's assumption that everything on the machine running as this user is already trusted. Exposing port 8876 beyond loopback would hand an unauthenticated caller SSH execution on every machine in the tailnet.

05.G

Continue