dsh-cc-computer-use-mcp
liufeicc/dsh-cc-computer-use-mcp
把本地 cc-computer-use MCP server 接入 DSH:每个根会话各起一个进程,各自拥有独立的 Xephyr 沙箱屏与私有 AT-SPI 总线(需已安装 cc-computer-use MCP server)。
安装
dsh plugin --profile web add github:liufeicc/dsh-cc-computer-use-mcpREADME
English | 中文
dsh-cc-computer-use-mcp
Wires a local cc-computer-use MCP server into DeepSeek Harness — one independent process per root session, so every session gets its own Xephyr sandbox screen and its own private AT-SPI bus.
What it adds
One Host plugin row in profile web:
id: cc-cu-per-agent
name: '@local/dsh-cc-computer-use-mcp'
It listens for agent/created and mounts one @deepseek-ai/dsh-mcp-client on each root agent's own
agent.ctx. The model tool table then gains 17 tools named mcp__cc-computer-use__<rawName>:
| Tool | Purpose |
|---|---|
get_ui_tree | Read the accessibility tree (structured text; the primary perception channel) |
get_screen_text | OCR text with coordinates (the perception channel for grey-area apps) |
find_element / element_info | Find elements by text/role/app |
click / double_click / type_text / press_key / scroll / drag | Element-first three-tier actions |
get_last_click_image | Look back at where the last coordinate click actually landed |
act_sequence | Submit a batch of actions in one call |
get_screen_layout / screenshot | Screen layout / screenshot |
list_windows / wait_window / launch_app | Enumerate windows / wait for one / launch an app on the target display |
Mounting follows the workspace MCP switch (since 2026-10-23)
These tools register in each agent's own layer (scope-local): the global lens cannot see them, so
@local/dsh-mcp-workspace-policy's MCP menu used to omit this server entirely and restrict could not hide
it. The two sides are now wired together:
- this plugin calls
declarePerAgent({ name })on the policy service, so the menu listscc-computer-usewith the same tool-count hint as any other server (the Host counts from the live sessions that mounted it); - unchecked means not mounted — no MCP process, no Xephyr, no AT-SPI bus (default off, matching global MCP servers);
- check/uncheck makes the policy emit
mcp-workspace/changed; this plugin mounts or callsfiber.dispose()for the live sessions in that workspace, so the child exits andatexitreclaims the screen; - when the policy service is absent (not installed, or an older version without the declaration API) it fails open and mounts as before — a missing policy must never take the user's tools away.
⚠️ After upgrading, check
cc-computer-usein the MCP menu for every workspace that needs computer control. Default-off means unselected workspaces no longer get these tools.
On plugin hot-reload the old fiber is stored on
agent[Symbol.for('dsh-cc-computer-use-mcp/mounted-fiber')]; the new instance adopts it and then keeps or
disposes it according to the latest selection — otherwise a second process would be mounted and hit
mcp-client's serverName reservation error.
Why one process per session
The problem
A cc-computer-use sandbox is one MCP server process = one Xephyr + one private AT-SPI bus. It was designed
around Claude Code's model (one MCP server child per session), but DSH's @deepseek-ai/dsh-mcp-client is
mounted at profile scope (scopeOf(ctx) ?? ctx.root), so the whole dsh process spawns exactly one server.
Every session therefore shared:
- the same Xephyr (one X11 pointer/keyboard focus),
- the same i3 window tree — a window A launched with
launch_appshows up in B'slist_windows, and B'sclickcan land on A's window, - the same
RefTable(ref numbers collide) and the sameScreenLock(one session holds it, the other is refused), - the same private AT-SPI bus.
Two agents using it at once fight over the sandbox — the "they're stealing my sandbox" symptom.
Why not several sandboxes in one process
That would use fewer processes, but it hits a hard libatspi constraint: atspi_init() is process-wide and
one-shot, so a process can bind exactly one AT-SPI bus, and exit + re-init against another bus is known
to corrupt its internal state (see _BUS_STALE_ERROR in cc-computer-use's backend/linux/atspi.py). Two
screens imply two buses, so two screens require two processes. This is also cc-computer-use's own design
assumption.
Why only root agents
A subagent is the same task's other hand and should work on the same screen (serialization is provided by
cc-computer-use's screen lock). DSH's tool view inherits along the scope ancestor chain
(ToolRuntime.view → chainLayers: the global layer plus every ancestor layer), so a subagent already sees
the tools mounted by its parent and must not start another process — that would leave one sandbox orphaned.
Three predicates identify a subagent; any one being true skips the mount: agent.parentAgent,
session.header.origin === 'subagent', session.header.delegationDepth > 0.
Lifecycle
agent.ctx is agent-local: "its contributions are agent-local, unwind on disposal". Closing a session unwinds
the plugin, which disconnects the MCP child, which exits and lets cc-computer-use's atexit reclaim its
Xephyr. No orphan screens.
Why the same serverName is safe
Tools register through ctx.tools.register as scope-local. DSH's own duplicate-name error spells out the
contract:
tool "X" is already registered (for a per-agent variant, register through that agent's agent.ctx instead)
The same reasoning holds for systemPrompt.section (a scoped section shadows a global one) and
mcpResources.register (layered by the calling scope), so many agents can share cc-computer-use without
colliding, and the model-facing tool names stay stable.
Installation / new-environment notes
-
The cc-computer-use frozen artifact must exist at
config.command(<cc-computer-use repo>/dist/computer-use-mcp). Re-run itsbuild.shafter changing its source. -
This package needs
@deepseek-ai/dsh-mcp-clientto be resolvable. Itspackage.jsondoes not declare it (profiles have no@deepseek-ai/*), so a symlink provides it:mkdir -p node_modules/@deepseek-ai ln -sfn <dsh install dir>/node_modules/@deepseek-ai/dsh-mcp-client \ node_modules/@deepseek-ai/dsh-mcp-clientnode_modules/is git-ignored; recreate the link on a new machine and after a DSH upgrade that moves the install directory. -
Profile
weblinks@local/dsh-cc-computer-use-mcphere, like the other local bundles.
Verifying two sessions get two screens
-
Call
mcp__cc-computer-use__get_ui_treeonce in each session (the sandbox starts lazily on first use). -
Inspect processes and display numbers:
ps -ef | grep -E "computer-use-mcp|Xephyr" | grep -v grep ls -d /tmp/cc-cu-at-spi-d* 2>/dev/nullExpect two
computer-use-mcpprocesses, two Xephyrs on different display numbers, and two distinct/tmp/cc-cu-at-spi-d<num>-*directories. -
Launch an app with
launch_appin session A and runlist_windowsin session B — B must not see A's window.
Offline test
node test/gating.mjs
No processes and no host required: a fake context exercises the gating logic (mount only when checked, dispose on uncheck, skip subagents, fail open without the policy service, adopt the old fiber on hot-reload).
License
MIT — see LICENSE.