A Model Context Protocol (MCP) server that lets AI agents (OpenCode, Claude Desktop, etc.) interact with a local desktop environment: screenshots, OCR with UI-element classification, mouse/keyboard control, and program launch — on Linux, macOS, and Windows.
12 tools registered under the da_* namespace:
da_screenshot— Capture full screen or a specific display as PNG.da_ocr— Run OCR (Tesseract) on a screenshot and return structured text + UI element classification.da_list_displays— List connected displays with id, bounds, scale factor.
da_get_mouse_position— Read current cursor position (X11/Linux usesxdotool getmouselocation --shell, Wayland usesydotool, macOS/Windows usesrobotjs).da_move_mouse— Move the cursor to (x, y).da_click— Click at (x, y) with optional button (left/right/middle/back/forward) and count.da_double_click— Convenience wrapper for double-click.da_drag— Drag from (x1, y1) to (x2, y2).da_scroll— Scroll wheel at (x, y) by (dx, dy).da_type— Type a string at the current focus.da_key— Press a single key or chord (e.g.Ctrl+C).
da_launch— Launch a program by name or path; returns a spawn handle with PID + POSIX signal exit codes (SIGINT=130, SIGTERM=143, SIGHUP=129, SIGKILL=137, SIGQUIT=131, SIGABRT=134).
The da_ocr classifier tags each detected text region with one of:
| Category | Examples |
|---|---|
button |
"OK", "Cancel", "Apply" |
input |
Text fields, search boxes |
label |
Static descriptive text |
checkbox |
"☑ Enable", "☐ Dark mode" |
radio |
"◉ Local", "○ Network" |
menu |
Top-level menu headers ("File", "Edit") |
menu-item |
Dropdown entries ("New", "Open…") |
icon |
Toolbar / sidebar icons |
- Language: TypeScript 7.0 (strict, ESM, Node 22+);
exactOptionalPropertyTypes,noUncheckedIndexedAccess,noFallthroughCasesInSwitchall on. Exact version pins (no^/~). - MCP SDK: v2 (
@modelcontextprotocol/server@2.0.0) overStdioServerTransport(production) andInMemoryTransport(tests). - Module layout (250 LOC ceiling per file):
- Screenshot —
src/screenshot/{png,backends,index,types}.ts. PNG validation/encoding isolated inpng.ts; backend dispatch (node-screenshots → screenshot-desktop → Windows CLI) inbackends.ts. - OCR —
src/ocr/{cli,index,mock,parse,wasm,types,classify,classify-rules}.ts. CLI backend (runCli), WASM fallback (runWasm), parser, mock; orchestrator inindex.tsrethrows asOCR_FAILEDwhen both backends fail. - Input —
src/input/{routing,mouse,keyboard,scroll,drag,types,index}.ts. Shared routing helpers (runCli,resolveRouting,requireTool,loadRobotjs,isMockMode,validateCoords,Routing) inrouting.ts; per-input-type operations in dedicated files. - Launch —
src/launch/{launch,types}.ts.open(1)+child_process.spawn(shell:false);SIGNAL_EXIT_CODESmap for POSIX signal mapping. - Platform —
src/platform/{detect,types}.ts.detectPlatform()returns{ os, display, tools, home };assertPlatformSupported()throwsPLATFORM_INIT_FAILEDon unsupported combos. - Server —
src/server.ts. Registers 12 tools, wraps handler results intoCallToolResultwithstructuredContent(Buffers stripped tonumber[]for JSON-safety), installs SIGINT/SIGTERM shutdown.
- Screenshot —
| Capability | Primary | Fallback 1 | Fallback 2 |
|---|---|---|---|
| Screenshot (Linux X11) | node-screenshots (XCap) |
screenshot-desktop |
— |
| Screenshot (Linux Wayland) | node-screenshots (XCap portal) |
screenshot-desktop |
— |
| Screenshot (macOS) | node-screenshots (CG) |
screencapture |
— |
| Screenshot (Windows) | node-screenshots (GDI) |
PowerShell BitBlt (windowsCliBackend, ships in backends.ts) |
— |
| Input (Linux X11) | xdotool CLI |
— | — |
| Input (Linux Wayland) | ydotool CLI |
wtype (keyboard only) |
— |
| Input (macOS / Windows) | robotjs (native) |
— | — |
| OCR (any OS) | tesseract CLI |
tesseract.js@7 WASM |
— |
Every spawnSync/spawn call uses shell:false. Permission-gated errors (e.g. macOS ScreenCaptureKit, Windows access denied) are detected via a tightened pattern (screen…permission, screencapturekit, access is denied) and re-thrown as DaMcpError('PERMISSION_DENIED').
# System dependencies (apt/dnf/brew; see scripts/install-system-deps.sh)
sudo ./scripts/install-system-deps.sh
# npm deps
npm install
# Build
npm run build
# Verify type-check (strict mode)
npm run typecheck
# Run all tests (mock mode — skips real native calls)
DA_MCP_TEST_MODE=mock npm testThe server speaks MCP over stdio. Configure your MCP client to launch node /projects/da-mcp/dist/server.js (or npx tsx src/server.ts for dev).
Set DA_MCP_TRANSPORT=http to expose the server on http://127.0.0.1:3000/<token>. A 256-bit random token is generated on first start and persisted at:
| OS | Token path |
|---|---|
| Linux | $XDG_CONFIG_HOME/da-mcp/token or ~/.config/da-mcp/token |
| macOS | ~/Library/Application Support/da-mcp/token |
| Windows | %APPDATA%\da-mcp\token |
The token file is created with mode 0o600 (owner-only). Rotate it any time:
node /projects/da-mcp/dist/server.js token regenerate
# → http://127.0.0.1:3000/<43-char-base64url-token>Override defaults with env vars:
DA_MCP_HTTP_HOST— bind address (default127.0.0.1); supports IPv4, IPv6 ([::1]), and hostnameDA_MCP_PORT— port (default3000)DA_MCP_TOKEN_PATH— override token storage path
The URL is unauthenticated token (bearer-style): anyone with the token can call tools. Bind only to 127.0.0.1 (default) — do not expose this to a network without adding an upstream auth proxy.
| OS | Screenshot | Input | Notes |
|---|---|---|---|
| Linux X11 | node-screenshots (X11 native) |
xdotool |
Requires libxtst-dev libpng-dev for robotjs build |
| Linux Wayland | node-screenshots (XCap portal) |
ydotool (daemon) |
XWayland fallback if available |
| macOS | node-screenshots (CG) |
robotjs (CGEvent) |
First call needs Screen Recording permission (TCC) |
| Windows | node-screenshots (GDI) |
robotjs (SendInput) |
VS Build Tools required; PowerShell BitBlt fallback if GDI fails |
# Strict type-check (no emit)
npx tsc --noEmit
# All tests in mock mode (CI default)
DA_MCP_TEST_MODE=mock npx vitest run
# Single test file
npx vitest run test/unit/screenshot.test.ts
# Watch mode
npx vitest- 18 test files: 15 unit (
test/unit/) + 3 e2e (test/e2e/) - 216 tests passing / 17 skipped in mock mode (e2e require real X11/tesseract)
- Test runtime:
process.env['DA_MCP_TEST_MODE'] === 'mock'short-circuits native calls;_mock.tsmodules inject deterministic native modules
- 250 LOC ceiling per file (measured as non-blank, non-comment lines:
awk '!/^[[:space:]]*$/ && !/^[[:space:]]*(\/\/|#|--)/' <file> | wc -l) - ESM imports use
.jssuffix even for.tssource - All
spawn*calls withshell: false - All native errors wrapped in
DaMcpErrorwith typedcodefromErrorCodeunion - Public surface re-exported from
src/screenshot/index.tsandsrc/input/index.ts— consumers import from there, not from per-operation files - Forbidden:
as any,@ts-ignore,@ts-expect-error,console.log,shell: true, auto-commits
DISPLAY— X11 display (Linux only)WAYLAND_DISPLAY— Wayland display socketDA_MCP_LOG— log level (trace|debug|info|warn|error), defaultinfoDA_MCP_TESSERACT_BIN— path totesseractbinary, defaulttesseractDA_MCP_OCR_BACKEND—cli(default) orwasmDA_MCP_TEST_MODE—mockskips real native calls in tests; e2e tests skip when setDA_MCP_SCREENSHOT_BACKEND— force a screenshot backend (node-screenshots|screenshot-desktop|windows-cli); default auto-detectDA_MCP_TRANSPORT—stdio(default) orhttp;httpenables the opt-in HTTP transportDA_MCP_PORT— HTTP port whenDA_MCP_TRANSPORT=http(default3000)DA_MCP_HTTP_HOST— HTTP bind address (default127.0.0.1); supports IPv4, IPv6, hostnameDA_MCP_TOKEN_PATH— override the auth token storage path
MIT
{ "mcpServers": { "da-mcp": { "command": "node", "args": ["/projects/da-mcp/dist/server.js"], "env": { "DISPLAY": ":0", "DA_MCP_LOG": "info" } } } }