Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ node_modules/

# Build outputs
dist/
!mcp-server/dist/
target/

# Environment
Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,55 @@ npm run tauri build

On macOS, you'll need to grant accessibility permissions when prompted (System Settings → Privacy & Security → Accessibility).

## Use with opencode CLI (new)

This repo now works headless via **MCP server** + **Agent Skill** for `opencode`, Claude Code, Cursor, etc. No Tauri window needed.

**1. Install skill (opencode):**
```bash
npx skills add suitedaces/computer-agent -g -a opencode -y
# or locally
npx skills add ./skills/computer-agent -g -a opencode -y
# verify
npx skills ls -g -a opencode
```

Skill lives at `skills/computer-agent/SKILL.md:1` and is discovered by opencode at `~/.config/opencode/skills` / `~/.agents/skills` (https://opencode.ai/docs/skills).

**2. Run MCP server:**
```bash
cd mcp-server
npm install # installs @modelcontextprotocol/sdk, screenshot-desktop
npm run build
# optional for mouse/keyboard:
npm install --include=optional # gets @nut/nut-js
node dist/index.js # stdio - test
```

**3. Wire into opencode - add to `opencode.json`:**
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"computer-agent": {
"type": "local",
"command": ["node", "mcp-server/dist/index.js"],
"enabled": true
}
}
}
```
See `opencode.json.example` and `mcp-server/README.md`.

**4. Prompt opencode:**
```
use computer-agent to take a screenshot and click the login button at 600,400
use computer-agent bash_run "ls -la"
use computer-agent browser_navigate https://example.com
```

The MCP exposes the same tools as the Tauri app: `computer_screenshot`, `computer_click` (AI 1280x800), `computer_type`, `computer_key`, `computer_scroll`, `computer_drag`, `bash_run` (blocked `rm -rf /` etc), `browser_snapshot`/`browser_navigate`/`browser_tabs` via `http://127.0.0.1:9222`. For full a11y `see_page` use the app Background Mode or `ego-browser`.

## Shortcuts

- `⌃⇧C` - push-to-talk → computer use mode
Expand Down
74 changes: 74 additions & 0 deletions mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# computer-agent MCP server

MCP server exposing `computer-agent` (taskhomie) tools to any MCP client - **opencode**, Claude Code, Cursor, etc.

Implements the same tool surface as `src-tauri/src/agent.rs` / `computer.rs` / `browser.rs` / `bash.rs` but over MCP stdio.

## Tools

| Tool | Mirrors | Description |
|------|---------|-------------|
| `computer_screenshot` | `ComputerControl::take_screenshot` | Base64 PNG, AI space 1280x800 |
| `computer_click` | `left_click`/`right_click`/`double_click` | AI coords 0-1280,0-800 |
| `computer_drag` | `left_click_drag` | from/to AI coords |
| `computer_type` | `type` | keyboard text |
| `computer_key` | `key`/`hold_key` | combo `ctrl+c`, `cmd+enter` |
| `computer_scroll` | `scroll` | direction + amount |
| `computer_move` | `mouse_move` | |
| `bash_run` | `BashExecutor::execute` | shell, blocked patterns, truncated |
| `browser_snapshot` | `BrowserClient::take_snapshot` | lightweight CDP via `http://127.0.0.1:9222` |
| `browser_navigate` | `navigate_page` | `url` or `back/forward/reload` |
| `browser_tabs` | `list_pages` | |
| `browser_click`/`browser_type` | `click`/`fill` | stub with fallback guidance |

For full a11y tree + `see_page`/`page_action`, run taskhomie app in **Background Mode** (Chrome ` --remote-debugging-port=9222`) or use `ego-browser` skill.

## Run

```bash
cd mcp-server
npm install
npm run build
node dist/index.js # stdio
```

## Use with opencode

Add to `opencode.json`:

```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"computer-agent": {
"type": "local",
"command": ["node", "mcp-server/dist/index.js"],
"enabled": true
}
}
}
```

Or from repo root:

```json
{
"mcp": {
"computer-agent": {
"type": "local",
"command": ["npx", "-y", "tsx", "mcp-server/src/index.ts"],
"enabled": true
}
}
}
```

Then prompt: `use computer-agent to take a screenshot and click the login button`

## Permissions

Give accessibility permission on macOS (System Settings -> Privacy & Security -> Accessibility) for mouse/keyboard to work. Install optional `@nut/nut-js` for full computer control:

```bash
npm install --include=optional
```
2 changes: 2 additions & 0 deletions mcp-server/dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};
239 changes: 239 additions & 0 deletions mcp-server/dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { takeScreenshot, mouseMove, click, drag, typeText, pressKey, scroll } from "./tools/computer.js";
import { bashRun } from "./tools/bash.js";
import { browserSnapshot, browserNavigate, browserTabs, browserClick, browserType } from "./tools/browser.js";
const server = new Server({
name: "computer-agent",
version: "0.1.0",
}, {
capabilities: {
tools: {},
},
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "computer_screenshot",
description: "Take a screenshot of the primary monitor. Returns base64 PNG (1280x800 AI space). Use before any mouse action to get coordinates.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
},
{
name: "computer_click",
description: "Click at AI coordinates (0-1280 x 0-800). Maps to real screen. Matches computer.rs left_click/right_click/double_click.",
inputSchema: {
type: "object",
properties: {
x: { type: "number", description: "AI x 0-1280" },
y: { type: "number", description: "AI y 0-800" },
button: { type: "string", enum: ["left", "right", "middle"], default: "left" },
double: { type: "boolean", description: "double click", default: false },
},
required: ["x", "y"],
},
},
{
name: "computer_drag",
description: "Drag from start to end (AI coords). Mirrors left_click_drag.",
inputSchema: {
type: "object",
properties: {
from_x: { type: "number" },
from_y: { type: "number" },
to_x: { type: "number" },
to_y: { type: "number" },
},
required: ["from_x", "from_y", "to_x", "to_y"],
},
},
{
name: "computer_type",
description: "Type text via keyboard (enigo / nut.js). Mirrors computer.rs type action.",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
},
},
{
name: "computer_key",
description: "Press key combo e.g. 'ctrl+c', 'cmd+enter', 'escape', 'tab'. Mirrors key/hold_key.",
inputSchema: {
type: "object",
properties: { combo: { type: "string" } },
required: ["combo"],
},
},
{
name: "computer_scroll",
description: "Scroll at position. direction up/down/left/right, amount 1-10. Mirrors scroll.",
inputSchema: {
type: "object",
properties: {
x: { type: "number", default: 640 },
y: { type: "number", default: 400 },
direction: { type: "string", enum: ["up", "down", "left", "right"] },
amount: { type: "number", default: 3 },
},
required: ["direction"],
},
},
{
name: "computer_move",
description: "Move mouse to AI coordinates without clicking.",
inputSchema: {
type: "object",
properties: { x: { type: "number" }, y: { type: "number" } },
required: ["x", "y"],
},
},
{
name: "bash_run",
description: "Run a shell command (blocked patterns filtered like bash.rs). Returns stdout/stderr + exit code. Use for terminal tasks (Background Mode).",
inputSchema: {
type: "object",
properties: {
command: { type: "string" },
timeout_ms: { type: "number", default: 30000 },
},
required: ["command"],
},
},
{
name: "browser_snapshot",
description: "Get browser pages / lightweight snapshot via CDP (http://127.0.0.1:9222). For full a11y tree use taskhomie app Background Mode.",
inputSchema: {
type: "object",
properties: { verbose: { type: "boolean", default: false } },
},
},
{
name: "browser_navigate",
description: "Navigate browser. Provide url to open new tab, or action back/forward/reload.",
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
action: { type: "string", enum: ["back", "forward", "reload"] },
},
},
},
{
name: "browser_tabs",
description: "List open Chrome tabs via CDP.",
inputSchema: { type: "object", properties: {} },
},
{
name: "browser_click",
description: "Click a11y element by uid (from browser_snapshot). Stub - falls back to computer_click guidance.",
inputSchema: {
type: "object",
properties: { uid: { type: "string" } },
required: ["uid"],
},
},
{
name: "browser_type",
description: "Type into a11y element.",
inputSchema: {
type: "object",
properties: { uid: { type: "string" }, text: { type: "string" } },
required: ["uid", "text"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "computer_screenshot": {
const b64 = await takeScreenshot();
return {
content: [
{ type: "text", text: `Screenshot taken (${b64.length} b64 chars, AI space 1280x800).` },
{ type: "image", data: b64, mimeType: "image/png" },
],
};
}
case "computer_click": {
const { x, y, button, double } = z.object({ x: z.number(), y: z.number(), button: z.enum(["left", "right", "middle"]).optional(), double: z.boolean().optional() }).parse(args);
const res = await click(x, y, button ?? "left", !!double);
return { content: [{ type: "text", text: res }] };
}
case "computer_drag": {
const { from_x, from_y, to_x, to_y } = z.object({ from_x: z.number(), from_y: z.number(), to_x: z.number(), to_y: z.number() }).parse(args);
const res = await drag([from_x, from_y], [to_x, to_y]);
return { content: [{ type: "text", text: res }] };
}
case "computer_type": {
const { text } = z.object({ text: z.string() }).parse(args);
const res = await typeText(text);
return { content: [{ type: "text", text: res }] };
}
case "computer_key": {
const { combo } = z.object({ combo: z.string() }).parse(args);
const res = await pressKey(combo);
return { content: [{ type: "text", text: res }] };
}
case "computer_scroll": {
const { x, y, direction, amount } = z.object({ x: z.number().optional(), y: z.number().optional(), direction: z.string(), amount: z.number().optional() }).parse(args);
const res = await scroll(x ?? 640, y ?? 400, direction, amount ?? 3);
return { content: [{ type: "text", text: res }] };
}
case "computer_move": {
const { x, y } = z.object({ x: z.number(), y: z.number() }).parse(args);
const res = await mouseMove(x, y);
return { content: [{ type: "text", text: res }] };
}
case "bash_run": {
const { command, timeout_ms } = z.object({ command: z.string(), timeout_ms: z.number().optional() }).parse(args);
const out = await bashRun(command, timeout_ms ?? 30000);
return { content: [{ type: "text", text: out }] };
}
case "browser_snapshot": {
const { verbose } = z.object({ verbose: z.boolean().optional() }).parse(args ?? {});
const out = await browserSnapshot(!!verbose);
return { content: [{ type: "text", text: out }] };
}
case "browser_navigate": {
const { url, action } = z.object({ url: z.string().optional(), action: z.string().optional() }).parse(args ?? {});
const out = await browserNavigate(url, action);
return { content: [{ type: "text", text: out }] };
}
case "browser_tabs": {
const out = await browserTabs();
return { content: [{ type: "text", text: out }] };
}
case "browser_click": {
const { uid } = z.object({ uid: z.string() }).parse(args);
const out = await browserClick(uid);
return { content: [{ type: "text", text: out }] };
}
case "browser_type": {
const { uid, text } = z.object({ uid: z.string(), text: z.string() }).parse(args);
const out = await browserType(uid, text);
return { content: [{ type: "text", text: out }] };
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
catch (e) {
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("computer-agent MCP server running on stdio");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
1 change: 1 addition & 0 deletions mcp-server/dist/tools/bash.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare function bashRun(command: string, timeoutMs?: number): Promise<string>;
Loading