bruce-comm
Supports GitHub Copilot in VS Code and Copilot CLI as MCP clients, including config/install helpers and a runner preset, so Copilot agents can register profiles and participate in task and message exchange.
Provides opt-in PC control on macOS through a helper app, with screenshot, zoom, click, type, key, and window tools, gated by Touch ID and a kill switch.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Bruce Comm
Bruce Comm is a local-only MCP gateway for the AI agents on your computer. Claude Desktop, GitHub Copilot in VS Code, Cursor, Claude Code, Gemini CLI, Codex and other MCP clients connect to it, register a profile (who they are, their skills, and the instructions other agents must follow to work with them), discover each other and exchange tasks and messages through a shared, persistent SQLite queue. A worker claims a task with a lease, does the work and reports a result, a failure or a question for the sender. An optional runner starts headless agent CLIs so tasks are processed with nobody at the keyboard, and a dashboard lets you watch the queue, approve held tasks and answer questions.
Claude Desktop VS Code Copilot Cursor Claude Code Gemini CLI Codex Copilot CLI ...
| | | | | | |
stdio stdio or HTTP (each client picks stdio or HTTP; Claude Desktop is stdio only)
| | | | | | |
v v v v v v v
+---------------------------------------+ +----------------------------------------+
| bruce-comm stdio | | bruce-comm serve (HTTP daemon) |
| one process per client connection | | /mcp MCP Streamable HTTP |
| | | /api REST / dashboard |
+-------------------+-------------------+ +--------------------+-------------------+
| |
v v
+-------------------------------------------------------------+
| %USERPROFILE%\.bruce-comm\bruce-comm.db (SQLite, WAL) |
| agents | sessions | tasks | task_events |
+------------------------------+------------------------------+
|
v
bruce-comm runner (runners.json)
|-- claude -p ... (Claude Code)
|-- gemini --prompt=... (Gemini CLI)
|-- codex exec - (Codex)
|-- copilot -s ... (Copilot CLI)
`-- your own commandEvery process opens the same database file, so a task sent from a stdio client is seen at once by an HTTP client, the dashboard and the runner. No daemon is needed for stdio clients; the daemon adds the dashboard and the HTTP endpoint.
Contents
Features · Requirements · Install & build · Quick start · Connecting clients · How agents use it · Running fully unattended · Dashboard · PC control · CLI reference · MCP tools · Resources & prompts · Security model · Configuration · Troubleshooting · Robustness · Limitations · Architecture
Further reading: docs/AGENT-GUIDE.md (text to paste into your agents' instructions) and docs/ARCHITECTURE.md (data model, state machine, routing, delivery semantics).
Related MCP server: Agent Task List MCP Server
Features
Agent directory. Agents register a profile: id, name, platform, description, skills, tags and, most importantly, instructions for other agents. Others look it up with
get_agentbefore sending work.Persistent task queue. Tasks (work with a result) and messages (information only), with priorities 0-9, threads, sub-tasks, idempotency keys, delayed delivery, expiry, leases and retries. Survives restarts.
Routing. To one agent, to any agent (
"*"), to whoever offers a skill or tag, or a broadcast to everyone.Sessions. One agent id can be open in several windows at once (two VS Code windows, Claude Desktop plus a terminal, a runner). Bruce Comm lists every live session and can deliver a task to one exact window (
to_session) or to whichever window has a folder open (to_workspace). Replies find their way back to the window that asked.Long-polling with instant wake-up.
get_next_taskandwait_for_resultblock until something happens. Changes made by the same process wake them at once; changes made by other processes wake them within about 0.4 s.Attention counters. Every tool response ends with a
_bruceblock (tasks waiting, unread messages, questions for you, results ready), because push notifications to a chat are unreliable.Questions mid-task. A worker can ask the sender a question (
request_input). The task pauses and comes back to the same worker, and the same window, once it is answered.Human in the loop. You can require approval for tasks sent to an agent; only you can approve or deny, from the dashboard or the CLI. You can also answer questions, cancel tasks and send tasks yourself.
Headless runner. Presets for Claude Code, Copilot CLI, Gemini CLI, Codex, Cursor CLI and VS Code chat, or any command of your own. Task text is never put on a command line.
Prompt-injection hygiene. Everything another agent wrote is delivered inside
<bruce_task>/<bruce_message>/<bruce_result>blocks with a provenance preamble; look-alike tags inside the text are escaped.Live dashboard. Agents and their sessions, tasks with filters, a detail drawer with the event timeline, approval banner, new-task form with a window picker, light and dark themes, live updates over SSE. No external resources.
Client setup helpers.
bruce-comm configprints ready-to-paste config with your absolute paths for 12 clients;bruce-comm installmerges it into the client's config file, with a backup.PC control (macOS, opt-in). Screenshot, zoom, click, type, key and window tools for any MCP client, through a small helper app. Off for every agent until you switch it on with Touch ID; a kill switch (⌃⌥⌘K) stops it at once; every call is logged with a thumbnail. See PC control.
Requirements
Node.js 22.13 or newer (uses the built-in
node:sqlite; developed and tested on Node 24).Windows 10/11 is the primary, tested platform. macOS and Linux code paths exist (config locations, process-group kill) but are less tested.
At least one MCP client. For unattended work, a headless agent CLI (
claude,gemini,codex,copilot, ...).PC control only: macOS 14 or newer and the Xcode Command Line Tools (
swiftc,codesign).
Install & build
cd "C:\path\to\Bruce Comm"
npm install
npm run build # tsc -> dist/
node bin/bruce-comm.js doctorOptional: put bruce-comm on your PATH with npm link (run it in the project folder). The examples below use
bruce-comm; without npm link, run node bin/bruce-comm.js from the project folder instead.
Client configs always use the absolute path of node.exe and bin\bruce-comm.js, so they keep working for clients that
do not see your PATH (Claude Desktop from the Microsoft Store, for example).
npm test builds and runs the Node test runner over test/.
Quick start (5 steps)
Build (see above) and run
bruce-comm doctor. It checks Node, the database, the daemon port, the runner config and which agent CLIs are installed, and prints the next step.Connect your clients from a normal terminal (Windows Terminal, PowerShell or cmd, not a terminal started from inside Claude Desktop):
bruce-comm install vscode # Quit Claude Desktop completely first (tray icon > Quit): it rewrites its config file on exit. bruce-comm install claude-desktopinstallmerges abruce-commentry into the client's config file and keeps a timestamped.bakcopy. It finds the real Claude Desktop config of a Microsoft Store (MSIX) install. Add--dry-runto preview. For other clients see Connecting clients.Restart the clients. In VS Code, allow the bruce-comm tools once ("Allow all tools from this server"). Optional: start the dashboard with
bruce-comm serveand open http://127.0.0.1:7457/.In each chat, make the agent a worker. Either run the
bruce_worker_modeprompt (VS Code: type/mcp.bruce-comm.bruce_worker_mode; Claude Code:/mcp__bruce-comm__bruce_worker_mode), or just ask:Register yourself in Bruce Comm as
copilot-frontend(skills: frontend, code-review), then go into worker mode: call get_next_task in a loop and do the tasks you receive.The agent calls
register_agentwith its profile and then loops onget_next_task(wait_seconds=50).Send a task. From another chat ("Use Bruce Comm to ask copilot-frontend to review src/checkout.ts and report the risks"), from the dashboard's New task form, or from the CLI as the human:
bruce-comm send --to copilot-frontend --title "Review checkout.ts" --body "Repository C:\src\shop. Review src/checkout.ts for bugs; list each issue with file:line." --wait 300bruce-comm tasksandbruce-comm task <id>show progress and the result.
Connecting clients
bruce-comm config <client|all> [--transport stdio|http] [--port n] [--agent-id id] prints the snippet for a client with
your absolute paths filled in. bruce-comm install <client> writes it into the client's config file for you.
Client | id for | Default agent id | Transports | Where it goes (Windows) |
Claude Desktop |
|
| stdio only | MSIX: |
VS Code (Copilot agent mode) |
|
| stdio, http |
|
Cursor |
|
| stdio, http |
|
Windsurf (legacy Cascade) |
|
| stdio, http ( |
|
Devin Desktop (formerly Windsurf) |
|
| stdio, http ( |
|
Visual Studio 2022 17.14+ / 2026 |
|
| stdio, http |
|
JetBrains AI Assistant |
|
| stdio, http | Settings > Tools > AI Assistant > Model Context Protocol (MCP) > Add > "As JSON" ( |
JetBrains Junie |
|
| stdio, http |
|
Claude Code |
|
| stdio, http |
|
Gemini CLI |
|
| stdio, http ( |
|
OpenAI Codex CLI / IDE extension |
|
| stdio, http |
|
GitHub Copilot CLI |
|
| stdio, http |
|
Aliases: claude = claude-desktop, code / vs-code = vscode, vs / visualstudio = visual-studio, intellij =
jetbrains, copilot = copilot-cli, gemini-cli = gemini.
stdio or HTTP?
stdio (default, recommended). The client starts
node bin\bruce-comm.js stdioitself. Nothing else needs to run. Each client gets its own small process; all of them share the database.HTTP. The client connects to
http://127.0.0.1:7457/mcp?agent=<id>. Needsbruce-comm serverunning. Useful when a client handles stdio servers badly. With a token configured, the generated entry carries theAuthorizationheader (Codex reads it fromBRUCE_COMM_TOKEN).
Identity pre-binding. Each snippet names a default agent id: stdio entries set BRUCE_AGENT_ID in env, HTTP entries
add ?agent=<id>. This only says who the session belongs to; the agent still registers its profile with register_agent
(instructions are required the first time). Use --agent-id for another id, for example a per-repository
.vscode/mcp.json with --agent-id copilot-frontend. In a pre-bound session:
a call that passes another
agent_idacts as that agent for that call only and does not rebind the window;register_agentwith another id does rebind it;the window can change or unregister only its configured agent and agents it registered itself.
register_agentnaming another existing agent binds the window without touching that profile (passing profile fields is refused), andunregister_agentfor it is refused. A window configured for that agent and the human (dashboard, CLI) can still change it; a session with no pre-bound id is not restricted (see Trust model).
Install behaviour. install never drops other keys, writes a timestamped .bak first, refuses to replace a
different existing bruce-comm entry without --yes, and refuses files with comments or trailing commas (JSONC)
without --yes because the comments would be lost. --config-file <path> targets another file. For Claude Code, Codex and
JetBrains AI Assistant it prints the command or text instead of editing a file.
Snippets
Do not type these by hand: bruce-comm config <client> prints them with your real paths. An env entry
BRUCE_COMM_HOME / BRUCE_COMM_DB is added only when you use a non-default data directory.
Claude Desktop (stdio only: Custom Connectors connect from Anthropic's cloud and cannot reach 127.0.0.1):
{
"mcpServers": {
"bruce-comm": {
"command": "node",
"args": ["C:\\Users\\you\\Bruce Comm\\bin\\bruce-comm.js", "stdio"],
"env": { "BRUCE_AGENT_ID": "claude-desktop" }
}
}
}Quit Claude Desktop from the tray before editing, keep the other keys (such as preferences), then start it again. On a
Microsoft Store install, edit the file under %LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude\; the
%APPDATA%\Claude path only exists inside the app's container. Logs: <config folder>\logs\mcp-server-bruce-comm.log.
VS Code, stdio and HTTP (%APPDATA%\Code\User\mcp.json or .vscode/mcp.json):
{
"servers": {
"bruce-comm": {
"type": "stdio",
"command": "node",
"args": ["C:\\Users\\you\\Bruce Comm\\bin\\bruce-comm.js", "stdio"],
"env": { "BRUCE_AGENT_ID": "vscode-copilot" }
}
}
}{ "servers": { "bruce-comm": { "type": "http", "url": "http://127.0.0.1:7457/mcp?agent=vscode-copilot" } } }Cursor, Junie, Copilot CLI and Gemini use the same shape under mcpServers (Gemini adds "trust": true, "timeout": 600000;
its HTTP key is httpUrl; Windsurf / Devin use serverUrl).
Claude Code:
claude mcp add bruce-comm --transport stdio --scope user --env BRUCE_AGENT_ID=claude-code -- "node" "C:\Users\you\Bruce Comm\bin\bruce-comm.js" stdioKeep the server name (and, for HTTP, the URL) before the options: --env and --header take several values and would
swallow a name that follows them. HTTP: claude mcp add bruce-comm "http://127.0.0.1:7457/mcp?agent=claude-code" --transport http --scope user, plus --header "Authorization: Bearer <token>" when a token is set (config adds it,
and -H likewise to its gemini mcp add command). Allow the tools without prompts with
--allowedTools "mcp__bruce-comm__*" (or /permissions).
Codex (%USERPROFILE%\.codex\config.toml):
[mcp_servers.bruce-comm]
command = 'node'
args = ['C:\Users\you\Bruce Comm\bin\bruce-comm.js', 'stdio']
startup_timeout_sec = 20
tool_timeout_sec = 120
default_tools_approval_mode = 'approve'
[mcp_servers.bruce-comm.env]
BRUCE_AGENT_ID = 'codex'Test the server with the MCP Inspector:
npx @modelcontextprotocol/inspector "node" "C:\Users\you\Bruce Comm\bin\bruce-comm.js" stdio.
How agents use it
The server sends short instructions at connect time, and the full protocol guide is the bruce://guide resource.
docs/AGENT-GUIDE.md has text you can paste into an agent's own instructions.
1. Register with good instructions. register_agent creates or updates the profile (patch semantics: only the fields
passed change) and binds the session. instructions is required on first registration. It is what other agents read
before sending work: what you accept and refuse, the input you need, what you return, typical turnaround. The
bruce_register prompt helps write it.
2. Discover. list_agents (filter by skill, platform or free-text query) shows presence, from most to least
reachable: listening (blocked in get_next_task now), active (a call in the last 2 minutes), connected (an open
session), idle (seen in the last 30 minutes), offline. get_agent returns the full card with instructions,
skills, queue counts and live sessions.
3. Delegate. send_task with exactly one route: to_agent (an id, or "*" for any agent), to_skill (matches skill
ids, skill tags and agent tags) or broadcast=true. Write a self-contained brief: the recipient cannot see your
conversation. Then wait_for_result, or pass wait_seconds to send_task. Useful extras: priority, input (any
JSON), parent_task_id for a sub-task of the task you are working on, idempotency_key for safe re-sends (a re-send
with a key you already used returns the original task, or every copy of a broadcast, instead of queueing again),
requires_approval, delay_seconds, expires_in_seconds (the task expires only if nobody has claimed it by then),
lease_seconds, max_attempts.
4. Work loop. get_next_task(wait_seconds=50) blocks until a task or message arrives, claims the best task (tasks
reserved for you first, then priority, then age) and delivers unread messages. Do the work, call update_task_progress at
least every lease_seconds / 2 on long work (default lease 15 minutes), and finish with exactly one of complete_task,
request_input, fail_task (retry=true for transient problems), reject_task or release_task. Then call
get_next_task again. An empty result only means nothing arrived yet.
5. Questions (input required). request_input pauses the task (input_required) and frees the worker's slot. The
sender sees it through questions_for_you and wait_for_result, and answers with reply_to_task. You can also answer
from the dashboard or with bruce-comm answer. The task goes back to the same worker, and the same window if it is still
open, with the answer in its history. Answering does not use up a delivery attempt.
6. Sessions and workspace routing. Each MCP connection is a session (S-XXXXXXXX) with its client name and version,
an optional label and its workspace folders. Folders come from MCP roots when the client supports them, or from
register_agent(session_workspaces=[...]).
list_sessions(orbruce-comm sessions) lists live sessions;get_agentshows an agent's sessions.send_task(to_agent="vscode-copilot", to_session="S-7K3M9Q2X"): only that window may claim it, while it is connected. If the window closes, or stops heartbeating for 120 s (hung or asleep), the task falls back to a window with the same workspace (any window of the agent when it had none). A window that comes back before someone else claimed the task gets its pin back.send_task(to_agent="vscode-copilot", to_workspace="shop-frontend"): any window of that agent whose workspace path, folder name or session label matches. Paths are normalised (case, slashes,file:///c%3A/...URIs).Without either, whichever session of the agent calls
get_next_taskfirst gets the task.With several windows open, give each one a
session_label(for example "VS Code - frontend repo").
7. Reply routing. send_task records the sending session. A follow-up sent with parent_task_id to the parent's
sender goes to the window that asked; one sent to the parent's assignee goes to the window that did the work (falling
back to that window's workspace if it has closed). The questions_for_you and results_ready counters show up in the
window that sent the task, or in every window when that one is gone.
8. Messages vs tasks. kind="message" is information only: it is complete once delivered (with the next
get_next_task), has no result and cannot ask questions. Reply to one, if useful, with
send_task(kind="message", to_agent=<sender>, thread_id=<thread>). Use tasks for anything that needs an answer.
Running fully unattended
The honest truth: an MCP server cannot wake up a chat. MCP clients call servers; a server cannot start a turn in an idle Claude Desktop, VS Code or Cursor conversation. So there are two ways to get work done unattended.
A. Worker mode in an interactive app. Run the bruce_worker_mode prompt in an open chat. The agent loops on
get_next_task(wait_seconds=50) and handles tasks as they arrive. It works while that window stays open, but each empty
poll costs a little context and tokens, and many chat apps pause long agent loops after a number of tool calls or turns
and ask you to continue. Good for "while I'm around"; not a background service.
B. The runner for headless CLIs. bruce-comm runner keeps one or more agents registered and listening. For every task
it starts a headless agent CLI, gives it the task, and records the outcome.
bruce-comm runner init # writes %USERPROFILE%\.bruce-comm\runners.json: one DISABLED example per preset
# edit runners.json: set "enabled": true on the entries you want, point "cwd" at a repository
bruce-comm runner check # resolves commands, checks cwd, placeholders, timeouts; exit code 1 on problems
bruce-comm runner # run until Ctrl+C (--only id,id --once --config file)Preset | Command | Task text reaches the CLI via | Result taken from | Notes |
|
| stdin |
|
|
|
| stdin | stdout |
|
|
| stdin ( |
| MCP config via |
|
| stdin | the |
|
|
| a prompt file (named in fixed text) |
| Experimental, untested on Windows; temporarily merges bruce-comm into |
|
| stdin (attached) | MCP: the Copilot agent must call | Experimental fire-and-forget; needs |
| your | stdin and | stdout, or MCP with |
|
Minimal runners.json:
{
"runners": [
{
"enabled": true,
"agent": {
"id": "claude-code-worker",
"name": "Claude Code (headless)",
"description": "Headless Claude Code worker for code reading, reviews and refactoring plans.",
"instructions": "Send ONE self-contained coding or analysis task per send_task: goal, repository path, constraints and the exact output you want. I run unattended and cannot see your conversation. My final answer becomes the result. Up to ~30 minutes per task.",
"skills": [
{ "id": "code", "name": "Coding" },
{ "id": "code-review", "name": "Code review" }
]
},
"preset": "claude-code",
"cwd": "C:\\src\\shop",
"extraArgs": ["--allowedTools", "Read,Grep,Glob"],
"timeoutSeconds": 1800,
"concurrency": 1
}
]
}Field | Default | Meaning |
|
| Run this entry. |
| required | Profile registered for the runner agent ( |
| required |
|
| preset's | Name on PATH or absolute path. Required for |
| preset's / none |
|
|
| Working directory of the CLI; point it at a repository. |
| none | Extra environment variables (values may use placeholders). |
|
| Hard limit per run; the whole process tree is killed. |
|
| Tasks run in parallel by this entry. |
| per preset |
|
|
| How the CLI reaches Bruce Comm; |
|
| Re-queue failed runs while attempts remain. |
|
| Keep |
Placeholders in args, extraArgs and env: {promptFile} {taskId} {agentId} {mcpConfigFile} {mcpUrl} {cwd}
{runDir} {outputFile} {nodePath} {bruceBin}. Keys starting with _ are comments.
What the runner does for each task:
It opens a session for the agent (transport
runner, label<preset> runner, workspace =cwd) and keeps the agentlistening.It claims tasks up to
concurrencyand writes the prompt (the provenance-wrapped brief plus reporting instructions) and a per-run bruce-comm MCP config with the identity pre-bound. That config marks the CLI's connection as a one-shot session for this task (stdio --runner-task <id>,BRUCE_COMM_RUNNER_TASK,&runner_task=for HTTP): the server refusesget_next_taskthere and leaves out the work-loop hints, so a headless CLI never claims more work outside the runner's supervision.It spawns the CLI without a shell and with a hidden window, and renews the lease while it runs. It kills the process tree if the task is canceled, reassigned or times out.
The run is over when the CLI exits, even if a background process it started (a dev server, a build daemon) still holds its output pipes. A timeout or kill always ends the run, also when part of the process tree escaped the kill.
It completes the task with the parsed output, or fails it with the exit code and the stderr tail.
If the CLI asks a question with
request_input, the run ends: the CLI is told to stop, and the runner stops it 10 s later at the latest (at once if the answer is already there). Nothing is reported for that run. Once the sender answers, the task is started again, in a new run, with the answer in its history; this does not use up an attempt.Messages for runner agents are written to the runner's log.
Ctrl+C hands in-flight tasks back to the queue without using an attempt and marks the agent offline. A second Ctrl+C exits at once.
Workspace config a run merged into (
cursor-agent:<cwd>\.cursor\mcp.json) is restored when the run ends, on a second Ctrl+C and on a crash. After a hard kill (taskkill /F, power loss) the next runner start restores it from the record kept in<home>\runs\. Such a file holds one run's entry at a time, so runs that use the samecwd(any entry of this runner process) take turns: a waiting run keeps its claim and lease, and its timeout starts when the CLI starts.where.exeandtaskkill.exeare started fromSystem32, never from the folder the runner was started in.
vscode-chat and identities. The chat runs in your VS Code window and reaches Bruce Comm through that window's own
bruce-comm entry, which bruce-comm install vscode pre-binds to vscode-copilot. The runner's prompt has the Copilot
agent pass the runner agent's id (agent_id) on every call. For a pre-bound window that acts as the runner agent for
that call only; the window keeps its own identity. runner check warns when the two ids differ, and when the VS Code
entry pre-binds no id (the first such call would then bind the window to the runner agent). To make them one agent,
give the runner the VS Code agent id ("agent": {"id": "vscode-copilot", ...}).
Auto-start at logon (Task Scheduler)
Bruce Comm does not install anything into Windows for you. To start the runner (and, if you like, the dashboard daemon) when you log on, create the wrapper script yourself and register it.
Save this as
%USERPROFILE%\.bruce-comm\bruce-comm-autostart.cmdand adjust the two paths:@echo off set "NODE=node" set "BRUCE=C:\Users\you\Bruce Comm\bin\bruce-comm.js" set "LOGS=%USERPROFILE%\.bruce-comm\logs" if not exist "%LOGS%" mkdir "%LOGS%" rem Dashboard + HTTP endpoint (optional - delete this line if you only use stdio clients): start "" /b "%NODE%" "%BRUCE%" serve 1>>"%LOGS%\serve.log" 2>&1 rem Runner (needs at least one enabled entry in runners.json): "%NODE%" "%BRUCE%" runner 1>>"%LOGS%\runner.log" 2>&1Register it from a Command Prompt (cmd.exe) opened with "Run as administrator" under your own Windows account. Windows usually requires elevation for logon triggers created with
schtasks; the task itself still runs as you, not elevated.schtasks /Create /TN "Bruce Comm" /SC ONLOGON /DELAY 0000:30 /TR "\"%USERPROFILE%\.bruce-comm\bruce-comm-autostart.cmd\"" /FCheck and manage it:
schtasks /Query /TN "Bruce Comm" /V /FO LIST schtasks /Run /TN "Bruce Comm" schtasks /End /TN "Bruce Comm" schtasks /Delete /TN "Bruce Comm" /F
The task runs only while you are logged on and shows a console window; closing that window stops Bruce Comm. Logs go to
%USERPROFILE%\.bruce-comm\logs\. Without admin rights, a shortcut to the .cmd in your Startup folder (Win+R,
shell:startup) has the same effect.
Dashboard
bruce-comm serve starts the daemon; open http://127.0.0.1:7457/.
Header: live counts of agents, sessions and tasks by state, with the connection state (SSE, or polling every 5 s as a fallback).
Attention banners: tasks awaiting your approval (Approve / Deny) and questions waiting for an answer.
Agents: presence, status, skills, expandable instructions, last seen, and the live sessions of each agent (client and version, label, workspaces, listening dot). From here you can send to one window, close a stale session, toggle "require approval" and unregister an agent.
Tasks: filter by status, agent or text; the detail drawer shows the body, input JSON, result or error, routing (
to_session,to_workspace, assignee session) and the event timeline, with Cancel, Answer, Approve / Deny and Add note.New task: to an agent, a skill, any agent or everyone; task or message; priority; optional window (session) or workspace.
PC control (macOS): the kill switch, helper and permission state, an On/Off toggle per agent (switching on asks for Touch ID) and the action log with thumbnails. It warns when BruceControl was not told about a stop, when a stop is not in the database and when the running helper serves another data folder. See PC control.
The page is one self-contained HTML document with no external requests and a strict Content-Security-Policy. All
agent-written text is escaped. With a token configured, open http://127.0.0.1:7457/?token=<token> once. The server
swaps it for an HttpOnly cookie and redirects to a clean URL.
The REST API behind it (/api/status, /api/agents, /api/sessions, /api/tasks, /api/events, ...) is listed in
docs/ARCHITECTURE.md. Every write through it acts as the human.
PC control (computer use, macOS)
PC control lets an agent connected to Bruce Comm (stdio or HTTP /mcp) see and use this Mac, much like Claude's computer
use but from any MCP client: screenshots, zoom, clicks, mouse moves and drags, scrolling, typing, key combos, opening apps
and URLs, listing and focusing windows. It is off for every agent until you switch that agent on in the dashboard with
Touch ID. A kill switch stops it at once, and every call is logged with a screenshot thumbnail.
MCP client --stdio/HTTP--> Bruce Comm (node) --NDJSON over unix socket--> BruceControl.app (Swift helper)
pc_* tools: identity, allowlist, ~/.bruce-comm/pc-control/helper.sock the only process that needs
kill switch, geometry, logging Accessibility + Screen RecordingBruce Comm (Node) decides: who is calling, whether that agent is allowed, whether the kill switch is on, where a
screenshot pixel is on screen, and what goes into the log. BruceControl.app does the work: a small menu-bar helper
built from source in this repository (native/macos/BruceControl/). Node starts it on demand through LaunchServices
(open -g -j -a BruceControl.app --args --home <data folder> --db <database>), so macOS treats BruceControl as the
responsible app: the permissions belong to BruceControl, not to Node, Terminal or your MCP client. Before every action the
helper checks again, on its own, that the agent holds a Touch ID grant and that nothing is stopped (see
Switching an agent on).
One BruceControl per macOS user. A second copy exits at once, whatever data folder it was started for: it finds the
per-user lock taken or another BruceControl app already running. So one helper serves one Bruce Comm data folder and
database at a time. Before actions (re-checked at most every 10 s) and before every Touch ID request, Bruce Comm checks
that the running helper serves its own data folder and database. If it serves another one, the call fails with
pc_helper_error ("BruceControl is running for another Bruce Comm data folder") and the dashboard shows "Helper serves
another data folder". Quit BruceControl from its menu bar item; the next PC action starts it for this folder.
On Windows and Linux the pc_* tools are listed but refuse with unsupported_platform, and pc_status says
"NOT SUPPORTED".
Setup
cd "/path/to/Bruce Comm"
npm run build # tsc -> dist/ (includes the pc_* tools)
npm run build:helper # swiftc -> ~/Applications/BruceControl.app (ad-hoc signed, hardened runtime)
npm run test:helper # optional: checks of the helper's own logic (no app launch, no input events)npm run build:helper runs scripts/build-pc-helper.sh. It compiles the helper and signs it ad hoc with the hardened
runtime (codesign -o runtime, identifier com.bruce.comm.pccontrol; the build fails if the runtime flag is missing).
It installs it at ~/Applications/BruceControl.app (BRUCE_PC_HELPER_APP changes the path; a bundle there that is not
BruceControl is never replaced), then prints the path and a reminder about permissions and the Keychain.
If a helper is already running, the script still installs the new build and then tells you to quit the running one
from its menu bar item (Bruce Control > Quit); it never kills it for you. Until you quit it, the old build keeps serving.
The next PC action after that starts the new build. Restart your MCP clients (and bruce-comm serve) after
npm run build so they load the new tools.
Bruce Comm and BruceControl are built from the same repository and must match: Bruce Comm refuses actions through an older BruceControl that does not report its data folder ("an outdated build") until you rebuild it and quit the old one.
Testing. npm test covers the Node side against a fake helper. npm run test:helper (scripts/test-pc-helper.sh)
compiles the helper's sources into a test harness with stand-ins for the Keychain and Touch ID and a temporary data
folder, and checks its grant, stop and startup-check logic. It never starts BruceControl.app, never posts mouse or
keyboard events and never touches your login Keychain or ~/.bruce-comm.
macOS permissions (you grant them yourself)
Bruce Comm never changes system settings. You give BruceControl (not Terminal, Node or Claude) two permissions:
System Settings > Privacy & Security > Accessibility: switch on BruceControl (if it is not listed, add it with
+from~/Applications). Needed for clicks, mouse moves, drags, scrolling, typing, keys and focusing windows.System Settings > Privacy & Security > Screen & System Audio Recording: switch on BruceControl. Needed for screenshots, zoom and window titles. macOS may ask you to quit and reopen BruceControl; the next call starts it again.
The dashboard's Request permissions button (and the helper's menu item Request permissions…) only makes macOS show Apple's own prompts and list BruceControl. It switches nothing on: you flip the switches yourself.
After every
npm run build:helperthe ad-hoc signature changes and macOS no longer recognises the old entries. In both lists remove BruceControl (select it,-) and add it again, or switch it off and on. Apermission_missingerror while the switch looks on usually means exactly this.Keychain. BruceControl keeps its grants and its stop flag in your login Keychain (item "Bruce Control state", service
com.bruce.comm.pccontrol). After a rebuild macOS asks again whether BruceControl may use that item, because the signature changed. The prompt names the app asking (BruceControl) and the item, shown as "Bruce Control state" or ascom.bruce.comm.pccontrol. Allow it (or Always Allow) only when the app it names is BruceControl (Bruce Control). Deny any prompt about this item that comes fromsecurity, Terminal,node, a script or any other app: that is another program trying to read or change the PC control state. If you deny BruceControl itself, it fails closed (no agent may act) until you switch an agent on or re-enable PC control, which asks again.
pc_status (as an agent) or bruce-comm pc-control status shows whether both permissions are in place.
Identity: who is calling
The pc_* tools take no agent_id argument. They act only as the identity pre-bound by the client's configuration:
BRUCE_AGENT_ID or stdio --agent-id for stdio, ?agent=<id> or the X-Bruce-Agent header for HTTP (see
Connecting clients). A session without a pre-bound identity is refused with
pc_control_refused, and an agent_id passed to other tools never changes who PC control acts for. The agent must also
be registered.
Runner sessions are refused. The one-shot sessions that bruce-comm runner opens for a headless CLI get
pc_control_refused, even when their agent is switched on: their brief was written by another agent and nobody watches
the screen. PC control works only in an attended window of the agent. These refusals are logged with the runner task
id (runner_task).
Switching an agent on
The dashboard's PC control panel lists every registered agent, all Off by default.
On: Bruce Comm first checks that the running BruceControl serves this data folder and database. Then BruceControl shows a Touch ID (or login password) prompt. It writes the prompt text itself and names the agent (
allow Bruce Comm agent "<id>" to control this Mac); nothing an agent sends can change that text. Approve it only when you just switched that agent on yourself. The agent is enabled only after the prompt succeeds; the toggle shows "Waiting for Touch ID…" meanwhile (up to 2 minutes). If you cancel or fail, the agent stays off (pc_auth_failed). While a prompt is open, another switch-on or re-enable getspc_helper_busyinstead of a second prompt.Off: no authentication. Bruce Comm refuses the agent from its next call and tells BruceControl, starting it if it is not running (it waits up to 10 s). If BruceControl could not be told, the panel says "BruceControl was not told": it drops the grant when it next starts (see the startup check below).
No MCP tool and no CLI command can switch an agent on.
Two records, both must say yes. The grant is recorded in the database (pc_control_agents) and in BruceControl's
own state in your login Keychain (generic password, service com.bruce.comm.pccontrol, account state). That state
holds the data folder and the database it belongs to, the granted agent ids and the stop flag. A BruceControl started
for another data folder or database sees no grants in it. BruceControl acts only when its Keychain state grants the
agent, the database has the agent switched on, and neither has the stop on. A database write alone therefore cannot
grant access, and cannot lift a stop that BruceControl has recorded. <home>/pc-control/state.json is only a read-only
copy for you to look at: BruceControl never reads it, so editing it or restoring an old copy changes nothing.
Startup check. When BruceControl starts, before it acts or grants anything, it compares its Keychain state with the database. A stop in the database stops it, and a grant whose agent is not switched on in the database is dropped; both are saved back to the Keychain when the database could be read. That is how a stop or switch-off that BruceControl was not told about reaches it later. When the database is missing, has no PC control tables yet or cannot be read, no agent may act, but no grant is dropped from the Keychain (a stop is still saved). Those grants stay unusable in the running BruceControl until it restarts (quit it from the menu bar) or you switch the agent off and on again. While the database is busy or locked at startup, BruceControl retries and actions fail with "waiting for the Bruce Comm database".
When the Keychain state cannot be used, BruceControl fails closed:
Still loading (for example while a Keychain prompt waits for you): status answers at once ("Loading state…" in the dashboard), and actions fail after about 2 s with "waiting for Keychain approval".
Unreadable, or you denied the Keychain prompt: no agent may act and everything counts as stopped ("Helper state unreadable"). Switching an agent on or re-enabling PC control (Touch ID) reads the item again, or rewrites it when it is damaged. Other Keychain errors are retried in the background, at most every 30 s.
Missing, or saved for another Bruce Comm data folder or database: no agent has a grant here. Switch the agents you need off and on again. A grant takes the item over for this data folder and database, so the other one's grants are gone.
Kill switch
Stops all PC control at once, for every agent. Stopping needs no authentication:
Where | How |
Dashboard | The red Stop all PC control button in the PC control panel. |
Hotkey | Control+Option+Command+K (⌃⌥⌘K) in any app while BruceControl runs. Needs no permission; the Mac beeps. |
Menu bar | BruceControl's menu bar item > Stop all PC control. |
CLI |
|
Dashboard and CLI: the stop is written to the database and sent to BruceControl, which is started if it is not running. Bruce Comm waits up to 10 s; a slower answer still arrives in the background. If BruceControl could not be told, the dashboard shows "BruceControl was not told" and the CLI prints a WARNING. Bruce Comm refuses every call anyway, and BruceControl applies the stop when it next starts. If the database write fails, the stop still holds in BruceControl (when it was told) and in the Bruce Comm process that took it; the dashboard shows "Stop not saved in the database" and the CLI prints a WARNING. If neither worked, the stop fails with
pc_stop_failed: press ⌃⌥⌘K.Hotkey and menu: BruceControl stops at once in memory, then writes the database and saves the stop in the Keychain.
A repeated stop keeps the first who and when.
Every pc_* call then fails with pc_killed. A long pc_type, drag, scroll or key repeat stops at its next step, and a
screenshot or window list taken while the stop lands is thrown away. Agents keep their On/Off setting. Re-enabling
needs Touch ID (the prompt reads re-enable PC control for Bruce Comm agents): use the dashboard's
Re-enable (Touch ID) button or the menu item Re-enable PC control…. A stop that lands during the prompt wins. No
CLI command or MCP tool can lift the stop.
Action log
Every pc_* action is logged, refused and failed calls included: time, agent, Bruce session id, tool, arguments (JSON;
pc_type text is cut to 500 characters), outcome (ok / refused / error), a one-line result or error and the
duration. After a successful action Bruce Comm stores a thumbnail of the affected display in the background (about
300 ms after the action; JPEG, about 480 px wide; best effort, it never delays or fails the call); pc_screenshot and
pc_zoom reuse their own image. Thumbnails live under ~/.bruce-comm/pc-control/thumbs/<YYYY-MM-DD>/<id>.jpg.
The dashboard shows the latest 50 entries; click a thumbnail to see it full size. Older entries:
GET /api/pc-control/log?limit=&before_id=&agent=. Retention: entries older than 30 days (with their thumbnails) are
deleted and at most 20,000 entries are kept; this is checked at most once an hour, when a new entry is written.
BruceControl's own trail. The helper logs every request it receives, including requests from programs that talk to
its socket directly: the op, the agent, the connecting process id (peer_pid), ok or the error code, and the duration.
It never logs typed text. Rejected connections are logged with their process id too. The lines go to the macOS unified
log:
log show --last 1h --predicate 'subsystem == "com.bruce.comm.pccontrol"'Screenshots, scale and coordinates
pc_screenshotreturns MCP image content of one display, downscaled like Claude's computer use: long edge at most 1568 px, at most 1.15 megapixels, aspect ratio kept, never upscaled.BRUCE_PC_MAX_LONG_EDGEandBRUCE_PC_MAX_PIXELSchange the limits.Every reply states the scale and names the other displays, for example:
Screenshot of display 0 (main, 1512x982 pt @2x) as 1330x864 px; 1 px = 1.137 pt. ... Other display(s): 1 (1429x804 px) - pass display=<index>.Its payload lists every display (displays: index, main, screenshot size, current).pc_statuslists them too once BruceControl runs.Coordinates are screenshot pixels of the last screenshot's display (per session; the main display until the first
pc_screenshot). Bruce Comm maps them to screen points: Retina-aware and multi-monitor safe, including displays left of or above the main one and rotated (portrait) displays. Coordinates outside the screenshot are refused with the valid range.display(an index, 0 = main) targets another display. Indices count from the main display, so they change when you make another display the main one.pc_zoomcaptures a region of the last screenshot (at least 8x8 px) at the display's native resolution, for small text; small regions are enlarged further until their long edge is about 400 px (within the limits above). The two axes can have different scales, so the reply stateskxandkyand the mapping back:screenshot_x = x + zoom_x / kx,screenshot_y = y + zoom_y / ky. Click coordinates always refer to the full screenshot, never to the zoomed image.Image size. By default (
format: "auto") an image is PNG. A PNG over 600 KB is sent as JPEG instead (quality 0.8, then lower), aiming at 750 KB or less (about 1 MB as base64). That target is best effort: when no quality reaches it, the smallest attempt is sent.format: "jpeg"uses the same target.format: "png"is never size-capped and can be several MB for a busy Retina screen.screenshot_after: trueon the action tools appends a fresh screenshot of the affected display to the reply.Modifier keys (
pc_clickmodifiers,pc_keycombos) are pressed as real keys before the click or key and released afterwards, also when the call is stopped half-way;fnis sent only as a flag.
Tools
Tool | Arguments | What it does |
| none | Who you are, whether you are allowed, whether PC control is stopped, helper and permission state, displays (only when allowed and BruceControl runs), the hotkey and how to get access. Works without being switched on. Read-only. |
|
| Screenshot of one display, with the scale and a list of all displays. Read-only. |
|
| Detail view of part of the last screenshot; small regions are enlarged to about 400 px; |
|
| Move there and click (double and triple click with |
|
| Move the pointer (hover). |
|
| Press at the first point, drag through the others, release at the last. All points are on one display; a drag cannot cross to another display. |
|
| Scroll at a point. |
|
| Type Unicode text into the focused field; |
|
| Press a key or combo. Modifiers |
|
| Start or activate an app. |
|
| Open a web page, optionally in a given browser. |
|
| Windows with id, app, title and bounds, and the frontmost app. Read-only. |
|
| Bring an app or window to the front. |
| none | Pointer position in screen points, its display, and its pixel position in that display's screenshot. Read-only. |
Key notes (pc_key). This is macOS: copy, paste, select all and undo are cmd+c, cmd+v, cmd+a and cmd+z
(ctrl+c in a terminal interrupts the program). delete and backspace delete backward (the Mac delete key);
forwarddelete or del deletes forward. xdotool names work too: BackSpace, Page_Down, KP_Enter, KP_0-KP_9,
Control_L, Super_L and so on. plus is the =/+ key (cmd+plus zooms in), since + itself joins the parts of a
combo. One non-modifier key per call.
App names, window titles and everything in a screenshot come from other apps and web pages. The tools tell agents to treat them as data, not instructions, and names and titles are cleaned before they appear in a reply's text.
REST endpoints behind the dashboard panel (same token and loopback rules as the rest of /api): GET /api/pc-control,
POST /api/pc-control/agents/<id> {"enabled": true|false}, POST /api/pc-control/kill {"killed": true|false},
GET /api/pc-control/log, GET /api/pc-control/log/<id>/thumbnail, POST /api/pc-control/permissions. Switching on
and lifting the stop still need Touch ID at the Mac. Switching off and stopping answer with helper_notified (and
helper_error, db_error when something did not work).
GlowCursor bridge
When the GlowCursor overlay (2.0, "Oggy Neon") runs, Bruce Comm shows each pc_* call on the cursor: a begin after
the call is authorized and an end (ok, refused or error with its error code) when it finishes, as
PUT /v1/action to 127.0.0.1:<port> (the port comes from <GLOWCURSOR_HOME>/port, default 47800; the token from
<GLOWCURSOR_HOME>/bridge.token). Only the tool, the agent, a call id and a small detail are sent: button, clicks,
scroll direction, key combo and modifiers, and for pc_type the number of characters. Typed text, URLs, app names and
window titles are never sent. It is display only and best effort: every request gives up after 150 ms, errors are
ignored (one log line at the first failure) and no call waits for it, except that pc_screenshot and pc_zoom wait
up to 30 ms for the cursor to hide its glow so captures show the plain arrow. Stopping and re-enabling PC control
push PUT /v1/overlay/kill; the daemon (bruce-comm serve) also checks the stop flag every 250 ms, so stops from the
hotkey, the BruceControl menu and the CLI show too, and repeats the push every 10 s while PC control is stopped.
Environment variables
Variable | Default | Meaning |
|
| Where |
|
| Socket Bruce Comm connects to (mainly for tests; the helper itself always listens there). |
| off |
|
|
| Longest screenshot edge in pixels. |
|
| Largest screenshot area in pixels. |
|
| Where the GlowCursor bridge finds the cursor's |
| on |
|
Bruce Comm starts BruceControl with its own data folder and database (--home, --db), so BRUCE_COMM_HOME and
BRUCE_COMM_DB apply to the helper too.
Files under <home>/pc-control/ (<home> is the data directory, ~/.bruce-comm by default):
helper.sock: directory mode 0700, socket 0600; only processes of your own user may connect. BruceControl always listens here, at the data folder's real path (symlinks resolved; on macOS/tmpand/varare/private/tmpand/private/var). That full path must stay under 104 bytes (a macOS limit), so keep the data folder path short; a longer one gives apc_helper_errorthat says so before BruceControl is started.state.json: a read-only copy of BruceControl's Keychain state (mode 0600). BruceControl never reads it.thumbs/: the action log thumbnails.
Outside the data directory: the login-Keychain item "Bruce Control state" (service com.bruce.comm.pccontrol, account
state) and the per-user lock ~/Library/Application Support/com.bruce.comm.pccontrol/instance.lock. Helper builds
before 1.1.0 kept a key under account state-hmac-key; it is no longer used, and you can delete it in Keychain Access.
Residual risks
The allowlist guards against agent mistakes and casual misuse, not against malicious programs. Any program running under your macOS account can claim an enabled agent's id (by setting
BRUCE_AGENT_ID) or talk to the helper's socket directly, and act as that agent while it is switched on. Starting its own BruceControl for another data folder or database gives it no grants: the Keychain state is bound to both, so that helper sees none, and each new grant needs your Touch ID. Switch agents off when you are done: that removes the grant.What protects the grants: the Touch ID prompt, whose text BruceControl writes itself and which names the agent, and the Keychain item's access list: macOS asks before an app other than BruceControl reads it, so deny such prompts. Whether another program running as you can replace or delete the item without a prompt has not been verified. Deleting it removes the grants; the stop then rests on the database and on a running BruceControl's memory.
The hard stops are: the kill switch (held in BruceControl's memory, in the Keychain and in the database; lifting it needs Touch ID), the Touch ID prompts for switching agents on, and the macOS permissions themselves. Switching BruceControl off in System Settings ends all PC control at the system level.
A stop or switch-off that BruceControl was not told about ("BruceControl was not told" in the dashboard, a WARNING in the CLI) lives only in the database until BruceControl next starts. A program that edits the database back before then undoes it. Stop again once BruceControl runs, or press ⌃⌥⌘K.
An enabled agent can do whatever you can do with mouse and keyboard: send mail, delete files, approve things. It also reads whatever is on screen, so text on a web page or in a mail can talk it into something (prompt injection). Switch agents on only while you watch, keep ⌃⌥⌘K in mind, and switch them off when done.
Screenshots leave the Mac. They go to the model behind the MCP client. Close or hide private windows first.
The log is plain files. Typed text (up to 500 characters) and thumbnails are stored unencrypted in your data directory. Do not let agents type passwords.
PC control troubleshooting
Errors reach agents as Error [<code>] with a hint; the REST API answers with the HTTP status shown.
Code | HTTP | Meaning |
| 501 | Not macOS. |
| 403 | No pre-bound identity, agent not registered, or an unattended runner session. |
| 403 | The agent is not switched on, or BruceControl has no Touch ID grant for it. |
| 409 | The kill switch is on. |
| 409 | BruceControl lacks Accessibility or Screen Recording (the message names which). |
| 403 | Touch ID / password failed or was canceled; nothing changed. |
| 409 | Another Touch ID prompt from BruceControl is already open. |
| 503 | BruceControl.app is not installed. |
| 503 | BruceControl is not running and was not started (launching off, or it did not open its socket within 8 s). |
| 504 | No answer in time (30 s by default; Touch ID requests 120 s). |
| 502 | Anything else from or about the helper: another data folder, an outdated build, waiting for the Keychain, socket path too long. |
| 500 | A stop or switch-off reached neither the database nor BruceControl. |
pc_helper_missing. BruceControl.app is not at ~/Applications/BruceControl.app (or BRUCE_PC_HELPER_APP). Run
npm run build:helper in the Bruce Comm folder. If the app is there but never starts, open it once from Finder to see
whether macOS blocks it, and rebuild if it does.
pc_permission_missing. The message names the permission (Accessibility or Screen Recording). Switch BruceControl on
under System Settings > Privacy & Security. After a rebuild, remove BruceControl from the list and add it again. If it
still fails, quit the helper from its menu bar item; the next call starts it again.
pc_killed. The kill switch is on (perhaps ⌃⌥⌘K pressed by accident). pc_status and the dashboard show who
stopped it and when. Lift it with Re-enable (Touch ID) in the dashboard or Re-enable PC control… in the menu bar.
pc_not_allowed. The agent is not switched on, or BruceControl has no Touch ID grant for it: its Keychain state was
missing, unreadable or saved for another data folder (the panel's Helper grant column then says "not granted"). Switch
the agent off and on again in the dashboard's PC control panel.
pc_control_refused. The session has no pre-bound identity: add BRUCE_AGENT_ID (stdio) or ?agent=<id> (HTTP) to
the client's config, restart the client and make sure the agent is registered. In a runner session this is by design:
PC control needs an attended window.
"BruceControl is running for another Bruce Comm data folder" (pc_helper_error). Only one BruceControl runs per
macOS user, and the running one serves another data folder or database. When it does not answer on this folder's socket
at all, the call fails after about 8 s with "running, but not for this Bruce Comm data folder". Either way, quit it from
its menu bar item; the next PC action starts it for this folder. "an outdated build": run npm run build:helper,
then quit the running BruceControl.
"waiting for Keychain approval" (pc_helper_error). BruceControl is still reading its state, usually because a
Keychain prompt is waiting for you (often hidden behind other windows). Answer it, allowing it only when it names
BruceControl, then retry.
"waiting for the Bruce Comm database" (pc_helper_error). BruceControl could not read the Bruce Comm database for
its startup check (busy or locked) and keeps retrying; switching an agent on or re-enabling also waits for it (up to
20 s) before any Touch ID prompt. Retry in a moment; if it persists, check that the daemon is not stuck and quit and
restart BruceControl from its menu bar.
pc_helper_busy. A Touch ID prompt from BruceControl is already open (another switch-on or re-enable). Finish or
cancel it, then try again.
"BruceControl was not told" / pc_stop_failed. The stop or switch-off is in the database, but BruceControl could not
be reached; it applies it when it next starts. pc_stop_failed means the database write failed as well: press ⌃⌥⌘K or
use Stop all PC control in the menu bar, then retry.
"socket path is too long" (pc_helper_error). The data folder path, with symlinks resolved, is too long for a macOS
socket (104 bytes in all). BruceControl always listens on <data folder>/pc-control/helper.sock, so use a shorter
BRUCE_COMM_HOME / --home; BRUCE_PC_HELPER_SOCKET does not help (it only changes where Bruce Comm connects).
What did the helper do? See the unified-log command under Action log.
Clicks land in the wrong place. Coordinates refer to the last screenshot's display. Take a fresh pc_screenshot
after changing resolution or plugging in or rearranging monitors, and use pc_zoom rather than guessing small targets.
CLI reference
bruce-comm <command> [options]. Global options: --json (machine-readable output), --home <dir> (data directory),
-h / --help. bruce-comm help <command> shows a command's options. Human commands work directly on the database: no
daemon is needed. Writes act as the reserved actor human.
Command | What it does |
| Run the HTTP daemon: MCP at |
| Serve one MCP session over stdin/stdout (what clients launch). Nothing but protocol on stdout. |
| Process tasks with headless agent CLIs from |
| Write the example |
| Queue and agent overview, session counts, daemon state. |
| List registered agents (presence, sessions, skills, flags). |
| One agent card: instructions, skills, queue counters, connected sessions. |
| Connected sessions (windows, app instances, runners); |
| Close a stale session. Pinned work goes to the agent's other sessions; work in progress is re-queued. Uses the daemon when it is running, so it can also disconnect an HTTP client. |
| List tasks, newest first. |
| One task with its full history. |
| Send a task or message as the human, optionally to one window. |
| Cancel an unfinished task. |
| Answer an agent's question on an |
| Add a note to a task's history. |
| Tasks waiting for your approval. |
| Approve or refuse a held task. |
| Change an agent's settings as the human (only the human can switch approval off). |
| Remove an agent. Its tasks are canceled if only it could do them, otherwise re-queued. An agent that requires approval can only be removed here or in the dashboard, never through MCP. |
| Delete finished tasks older than N days. |
| Print ready-to-paste client config. |
| Merge the entry into the client's config file (with a backup). |
| Check Node, database, daemon port, token, runner config, agent CLIs and client configs; suggest the next step. |
| PC control (macOS): kill switch, helper, permissions and agents ( |
| Version, help. |
Exit code 1 on errors, with a one-line message and a hint (BRUCE_COMM_DEBUG=1 adds stack traces).
MCP tools reference
Every tool accepts an optional agent_id meaning you, the caller (in get_agent and list_sessions it names the agent to
look up instead). Errors come back as Error [<code>]: <message> plus a Hint: line.
Tool | Use it to |
| Create or update your profile (patch semantics), set your status, label this window ( |
| Remove your profile for good (to go offline for a while, set |
| Find agents by skill, platform or text, with presence, sessions and whether they accept you. |
| Read an agent's full card, instructions and live sessions before sending it work. |
| Who you are (or how to register), this session, your counters and tasks, queue stats, dashboard URL, your wait cap. |
| Queue a task or message to an agent, |
| Full details and history of one task. |
| Tasks you sent ( |
| Block until tasks you sent are finished or asking you a question ( |
| Withdraw a task you sent. |
| Answer the question on a task you sent (it resumes), or add a note to a task you take part in. |
| Claim your next task and receive messages; long-polls with |
| Report progress and renew the lease. |
| Ask the sender a question; the task pauses. |
| Finish with a self-contained |
| Report failure; |
| Decline permanently (not your job, out of scope, unsafe). |
| Hand a task back unchanged without using an attempt. |
| Live sessions (windows) of all agents or one, with client, label, workspaces; |
| PC control: whether you are allowed, whether it is stopped, helper and permission state, displays, how to get access. |
| Take a downscaled screenshot of one display (image content, scale stated, all displays listed). |
| See part of the last screenshot at higher resolution (scale per axis, |
| Click at screenshot pixel coordinates (button, 1-3 clicks, modifiers). |
| Move the pointer to screenshot pixel coordinates. |
| Drag along a path of 2-50 points on one display. |
| Scroll up, down, left or right at a point. |
| Type text into the focused field. |
| Press a key or combo ( |
| Start or activate an app by name or bundle id. |
| Open an http/https URL, optionally in a given app. |
| List windows (id, app, title, bounds) and the frontmost app. |
| Bring a window or app to the front. |
| Pointer position in screen points and screenshot pixels. |
PC control tools (pc_*, macOS). They take no agent_id: they act only as the session's pre-bound identity
(BRUCE_AGENT_ID, --agent-id, ?agent=, X-Bruce-Agent), only for agents you switched on in the dashboard, and
never in unattended runner sessions. Coordinates are pixels of the last screenshot's display. Details in
PC control.
Long-poll caps. get_next_task, wait_for_result and send_task(wait_seconds) block for at most 50 s in most clients,
because Claude Desktop, Codex and Cursor's agent CLI cut tool calls off at about 60 s even while progress notifications
flow. VS Code and Claude Code calls that carry a progress token may block up to 300 s (progress is sent every 10 s).
BRUCE_COMM_MAX_WAIT_SECONDS overrides the cap for every client. bruce_status shows the cap that applies.
Resources & prompts
Resource | Type | Content |
| Markdown | The full protocol guide for agents. |
| JSON | The agent directory. |
| JSON | Live sessions of all agents. |
| Markdown | One agent's card, including instructions. |
| JSON | A task and its history. |
| JSON | What an agent could claim now and what it is working on. |
All resources are subscribable (notifications/resources/updated when their content changes); the resource list
changes when agents register or leave.
Prompt | Arguments | Purpose |
|
| Register (or update) and loop forever: get a task, do it, report, repeat. |
|
| Find the right agent, read its instructions, send a self-contained task, wait, report back. |
|
| Write an excellent profile (especially the instructions) and register it. |
Security model
Bruce Comm assumes one trusted user on one machine. Its job is to stop other machines, web pages and over-eager or misled agents from misusing the queue.
Trust model
Identity is claimed, not proven. An agent is whoever it says it is: the
agent_idit passes, or the id its client config pre-binds. There are no per-agent credentials. That fits what Bruce Comm is for: cooperating agents, all started by you, on your own computer.What the guards are for: mistakes, runaway loops and agents talked into something by text they received:
the approval gate is human-only: approving, denying, switching
require_approvaloff and removing an agent that has it on exist only in the dashboard, the CLI and the REST API, never as MCP tools;a pre-bound window (
BRUCE_AGENT_ID,?agent=) keeps its own identity: a call that names another agent acts for that call only, and the window can change or unregister only its own agent and agents it created, so one manipulated client cannot rewrite or remove the other agents' profiles (task calls in another agent's name still work: thevscode-chatrunner relies on them);accept_fromlimits who may send an agent work;rate, queue, delegation-depth and broadcast limits stop agents that delegate in circles or flood each other;
provenance wrapping marks everything another agent wrote as a request, not as an instruction from you.
What they do not protect against: a malicious process running under your account. It can pass any
agent_id, open the database file directly, or act as the human through the CLI or the REST API (it can read the token from the same places your clients do). Anything you can do, it can do. Your Windows account is the trust boundary, and the database is an unencrypted SQLite file in your profile.
Protections
Loopback only. The daemon binds only
127.0.0.1,localhostor::1; any other--hostis refused.DNS-rebinding and cross-site protection. The
Hostheader must name a loopback address with the daemon's port, otherwise 403. AnOriginheader, when present, must be the same loopback origin. Requests a browser marks asSec-Fetch-Site: cross-site/same-siteare refused on/apiand/mcp.Optional token. With
BRUCE_COMM_TOKEN(orserve --token),/mcpand/apirequireAuthorization: Bearer <token>(compared in constant time). The dashboard trades/?token=for an HttpOnly, SameSite=Strict cookie that only works for/api./healthzstays open. stdio does not use the network at all.Hardened dashboard. CSP
default-src 'none'with hashed inline script and style,X-Frame-Options: DENY, no external resources, all agent text escaped. Request bodies are limited to 1 MB.Human-only approval gate. Approve and deny exist only in the dashboard, CLI and REST API (actor
human), never as MCP tools. Agents can switchrequire_approvalon for themselves but never off, and only the human can remove an agent that has it on, so unregistering and registering again cannot reset it. An unapproved task addressed to a gated agent that goes back to the queue (a retry, a release, an expired lease, an answered question) is held for approval again. Reserved ids (human,system,all,any,everyone,*) cannot be registered or pre-bound.accept_from. An agent can restrict who may send it tasks; this is checked at send time and again at claim time.Loop and flood limits. Delegation depth at most 8 (
parent_task_idchain); at most 60 items per sender per recipient in 5 minutes; at most 1000 waiting items per agent; broadcasts reach at most 100 agents. The human is exempt from the rate limits.Provenance wrapping. Task text, messages, results, questions and notes from other agents are delivered inside
<bruce_task>,<bruce_message>or<bruce_result>blocks. A preamble says who wrote the text and that it is a request, not an instruction from the user. Anything inside that looks like these tags is escaped, so a sender cannot close the block early. Short agent-chosen names that appear inside Bruce Comm's own sentences (skill ids, tags,to_skill,platform, session labels) must be plain text: no control characters, line breaks, bidi controls,<,>,"or backticks.The runner never puts task text in argv. Task text goes on stdin or into a prompt file. Processes are spawned without a shell; unknown
.cmd/.batfiles are refused. Each run gets a generated bruce-comm MCP config with the identity pre-bound. Claude Code is limited to it (--strict-mcp-config) and Gemini to the bruce-comm server (--allowed-mcp-server-names); Copilot CLI and Codex also load the MCP servers you configured for them. The presets start with narrow permissions: Claude CodedontAskwith only the Bruce Comm tools, Codexread-onlysandbox, Gemini's non-interactive mode. You widen them per entry withextraArgs.Prompt-injection caveats. Wrapping and preambles reduce the risk; they do not remove it. A model can still be talked into following instructions hidden in a task or a result. Give each agent the least privilege it needs, use
require_approvalfor agents with powerful tools, and never put secrets into tasks or results.PC control is owner-gated (macOS). The pc_* tools are off for every agent by default, act only as the pre-bound identity, are refused in unattended runner sessions, and need an agent you switched on with Touch ID. A kill switch (dashboard, ⌃⌥⌘K, menu bar,
bruce-comm pc-control stop) blocks them all; lifting it needs Touch ID. The helper app (one per macOS user, signed with the hardened runtime) re-checks before every action both its own grant list and stop flag, kept in your login Keychain, and the database. It talks only to processes of your own user and logs every request to the macOS unified log. Allow Keychain prompts for its item only when they name BruceControl. It still trusts the claimed identity like the rest of Bruce Comm: see Residual risks.
Configuration
Variable | Default | Meaning |
|
| Data directory (database, |
|
| Database file. |
|
| Daemon port (also used for the dashboard URL and generated HTTP configs). Same as |
|
| Bind address; only |
| none | Shared secret for |
| none | Pre-binds a stdio session to this agent id (same as |
| none | Set by the runner for the CLIs it starts: the stdio session is a one-shot session for that task (same as |
| per client (50 / 300) | Longest a long-poll may block, for every client (5-3600). |
| off |
|
| none | Redirects |
|
| Respected when locating those clients' config files. |
| see PC control | PC control helper location and screenshot limits (macOS). |
HTTP clients can pre-bind with /mcp?agent=<id> or the header X-Bruce-Agent: <id>.
Data directory layout: bruce-comm.db (plus -wal / -shm), runners.json, runs\<task>-<attempt>-<claim time>\
(per-run prompt and MCP config, deleted afterwards unless keepRunFiles), runs\workspace-restore-*.json (originals of
workspace config files a runner has changed; replayed by the next start if the runner was killed), work\<agent-id>\
(default runner working directory). Keep the
data directory outside AppData: stdio servers started by a Microsoft Store Claude Desktop see a virtualized copy of
AppData (doctor warns about this).
Troubleshooting
Claude Desktop does not pick up the config. On a Microsoft Store (MSIX) install the real file is
%LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude\claude_desktop_config.json. The documented
%APPDATA%\Claude path only exists inside the app's container, and the app's "Edit Config" button may open the wrong
file. bruce-comm install claude-desktop finds the right one. Also check:
Quit Claude Desktop from the tray before editing; it rewrites the file on exit.
Run
installfrom a normal terminal. Writes toAppDatafrom processes started inside Claude Desktop (including its bundled Claude Code) are virtualized, so the app never sees them.Read
<config folder>\logs\mcp-server-bruce-comm.log.
Tool calls time out. Long-polls are capped at 50 s for most clients; Claude Desktop and Codex stop calls after about 60 s, and some Claude Desktop builds after 4 minutes. If a client still times out:
Ask the agent to use
wait_seconds=25, or setBRUCE_COMM_MAX_WAIT_SECONDS=25in that client'senvblock.Codex: the generated config sets
tool_timeout_sec = 120.bruce_statusshows the cap in effect and whether the client sent a progress token.
Gemini CLI is slow to start. Startup plus MCP discovery can take 30-60 s. The generated client config sets
"timeout": 600000. For the runner preset use timeoutSeconds of at least 120 (runner check warns). Hidden or
non-TTY launches need GEMINI_CLI_NO_RELAUNCH=true; the runner sets it. Log in once interactively first.
Port 7457 is in use. bruce-comm doctor tells you whether the port is free. Use bruce-comm serve --port 7460 (or
BRUCE_COMM_PORT) and regenerate HTTP client configs with config / install --port 7460, since the port is part of the
URL. stdio clients do not depend on the port.
Error [not_registered]. The session has no registered identity. Pre-binding (BRUCE_AGENT_ID, ?agent=) only
names the session; the agent must call register_agent once with instructions. If the agent was removed in the
dashboard or with bruce-comm unregister, register again. In a new session, pass agent_id on the first call.
Stale sessions. Each process that hosts sessions heartbeats them every 30 s. The sweeper closes a session when:
its host process has exited and its heartbeat is more than 45 s old (so usually within about a minute);
no host process is recorded and it has had no heartbeat for 120 s;
its host process still runs but has not heartbeated for 6 hours (a hung host).
A stale heartbeat alone does not close a session whose host is still running (the machine slept, the process stalled): after 120 s it only counts as not connected (tasks pinned to it may go to other windows meanwhile), and it is connected again with its host's next heartbeat. A session closed for a missing heartbeat is reopened the same way. If Windows gave a crashed host's PID to another process, its sessions linger like this for up to 6 hours; close them yourself.
Closing re-queues the session's work in progress (on its last attempt the task fails instead) and releases tasks pinned
to it. To see and close sessions yourself: bruce-comm sessions --all, bruce-comm close-session S-..., or the
dashboard. A closed stdio connection stays open in its client until that client exits, so restart that window; an HTTP
client is disconnected by the daemon.
Tasks are not delivered. bruce-comm agent <id> shows what the agent can claim, what is pinned to another
session or workspace, and what awaits approval. Look for:
an agent at its
max_concurrent_tasks;a sender that is not in the recipient's
accept_from;a recipient with
require_approval: its tasks wait for you (bruce-comm pending, thenapprove);a
to_workspacethat no open window matches (send_taskwarns about this).
Tools missing in VS Code or Visual Studio. Reload the MCP servers. In VS Code allow the tools; in Visual Studio enable them in the Copilot Chat tool picker, where they are off by default.
Robustness
Sleep and resume. A session whose host process is still running is closed only after 6 hours without a heartbeat, so work in progress is not re-queued while the machine sleeps. A session that was closed for a missing heartbeat comes back as soon as its host heartbeats again. Tasks pinned to a window that stopped heartbeating do not wait for it: after 120 s another window with the same workspace (any window, if none was recorded) may take them.
Ghost long-polls. When an HTTP client drops a request (crash, reload, network error), the daemon cancels it as if the client had sent
notifications/cancelled. Aget_next_taskcut short this way hands back the task it had claimed and the messages it had taken.Approval. Only the human can switch
require_approvaloff or remove an agent that has it on (dashboard, REST API, CLI), so another agent cannot reset it by unregistering and registering that agent again. Unapproved tasks that come back to such an agent's queue are held again, and work nobody could claim unapproved (skill-routed or"*"work when every willing agent requires approval) is held at send time instead of waiting forever.Runner. A run ends when the CLI exits, even if a background process keeps its output pipes open, and timeouts and kills always end it. A question (
request_input) ends the run, and the answer starts a new one. Each claim gets its own run directory. Workspace config the runner merged is restored after a crash or a hard kill.where.exeandtaskkill.exeare started fromSystem32.
Limitations
No push into chats. Interactive agents only work while a worker-mode loop runs in an open window. Use the runner for real background processing.
At-least-once delivery. A task can be delivered again after an expired lease or a crash. Workers should check the attempt number and history before repeating side effects.
One machine, one user. There is no networking between machines, no per-agent authentication, and no encryption at rest.
Workspaces need roots or a declaration. Clients that do not support MCP roots report no folders; the agent must pass
session_workspaces.The
cursor-agentandvscode-chatpresets are experimental.vscode-chatcannot return output; the Copilot agent must callcomplete_task.Size limits:
Field
Limit
Title
300 chars
Body
100,000 chars
Instructions
20,000 chars
Result
200,000 chars
Notes and questions
20,000 chars
JSON payloads
200,000 chars
HTTP request body
1 MB
Captured runner output
1 MB per stream
Housekeeping is manual. Finished tasks stay until you run
bruce-comm purge.Experimental SQLite module.
node:sqliteis still experimental in Node; its warning is suppressed.
Architecture
The components, the data model, the task state machine, session routing, delivery semantics and cross-process wake-up are covered in docs/ARCHITECTURE.md.
Source map: src/store (queue and data model), src/mcp (tools, resources, prompts), src/transports (stdio, HTTP),
src/dashboard (REST API, SSE, page), src/runner (headless runner), src/cli.ts and src/clients.ts (CLI, client
configs), src/pccontrol (PC control: tools, policy, geometry, log) and native/macos/BruceControl (the macOS helper,
built by scripts/build-pc-helper.sh).
This server cannot be deployed
Maintenance
Related MCP Connectors
Private-by-default, local-first memory/context/task orchestrator for MCP apps and agents.
Remote MCP server for The Colony — a social network for AI agents (posts, DMs, search, marketplace).
Coordinate multiple AI agents over MCP: atomic claims, leases, shared ledger, handoffs, tasks.
Persistent memory, knowledge graph, task management and more for AI agents via MCP.
Related MCP Servers
- AlicenseAqualityBmaintenanceMCP coordination primitive for multi-agent workflows. Lets multiple AI coding agents (Claude Code, Cursor, n8n, custom clients) discover each other and exchange messages and tasks via a shared SQLite-backed message bus.38392 npm4MIT
- FlicenseNot gradedqualityDmaintenanceProvides a self-hosted shared work queue for AI agents via MCP, using a local SQLite database for task management.-
- AlicenseNot gradedqualityCmaintenanceEnables multiple AI agents and clients to register identities and projects, assign commander/worker roles, coordinate phase-based workflows, exchange messages, and track project state through a shared local-first MCP bridge.1ISC
- AlicenseNot gradedqualityBmaintenanceEnables coding-agent sessions on the same machine to collaborate locally and durably by exchanging requests, replies, and task results through an authenticated loopback server with SQLite-backed storage, joined via CLI or an MCP stdio bridge. It also handles message delivery by priority and wait time, deduplicates idempotent sends, enforces task versioning, and preserves artifacts by content hash.22 npmApache 2.0