@hasna/computer 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -2
- package/README.md +73 -7
- package/dist/apps/ghostty/applescript.d.ts +36 -0
- package/dist/apps/ghostty/applescript.d.ts.map +1 -0
- package/dist/apps/ghostty/applescript.test.d.ts +2 -0
- package/dist/apps/ghostty/applescript.test.d.ts.map +1 -0
- package/dist/apps/ghostty/driver.d.ts +10 -0
- package/dist/apps/ghostty/driver.d.ts.map +1 -0
- package/dist/apps/registry.d.ts +5 -0
- package/dist/apps/registry.d.ts.map +1 -0
- package/dist/apps/registry.test.d.ts +2 -0
- package/dist/apps/registry.test.d.ts.map +1 -0
- package/dist/apps/types.d.ts +47 -0
- package/dist/apps/types.d.ts.map +1 -0
- package/dist/cli/index.js +1056 -92
- package/dist/cli/storage.d.ts +3 -0
- package/dist/cli/storage.d.ts.map +1 -0
- package/dist/cli/storage.test.d.ts +2 -0
- package/dist/cli/storage.test.d.ts.map +1 -0
- package/dist/db/storage-sync.d.ts +57 -0
- package/dist/db/storage-sync.d.ts.map +1 -0
- package/dist/db/storage-sync.test.d.ts +2 -0
- package/dist/db/storage-sync.test.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -26
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.test.d.ts +2 -0
- package/dist/mcp/http.test.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +609 -262
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/server/index.js +34566 -8748
- package/dist/storage.d.ts +5 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +5519 -0
- package/package.json +7 -2
- package/dist/cli/cloud.d.ts +0 -3
- package/dist/cli/cloud.d.ts.map +0 -1
- package/dist/db/cloud-sync.d.ts +0 -33
- package/dist/db/cloud-sync.d.ts.map +0 -1
package/dist/mcp/index.js
CHANGED
|
@@ -4917,9 +4917,11 @@ var require_lib2 = __commonJS((exports, module) => {
|
|
|
4917
4917
|
});
|
|
4918
4918
|
|
|
4919
4919
|
// src/mcp/index.ts
|
|
4920
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4921
4920
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4922
4921
|
|
|
4922
|
+
// src/mcp/server.ts
|
|
4923
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4924
|
+
|
|
4923
4925
|
// node_modules/zod/v3/external.js
|
|
4924
4926
|
var exports_external = {};
|
|
4925
4927
|
__export(exports_external, {
|
|
@@ -19055,6 +19057,188 @@ function getAccessibilityHelperPath() {
|
|
|
19055
19057
|
throw new Error("Accessibility helper not found. Run `swiftc helpers/accessibility.swift -o helpers/accessibility -framework AppKit` from the project root.");
|
|
19056
19058
|
}
|
|
19057
19059
|
|
|
19060
|
+
// src/apps/ghostty/driver.ts
|
|
19061
|
+
import { existsSync as existsSync4 } from "fs";
|
|
19062
|
+
|
|
19063
|
+
// src/apps/ghostty/applescript.ts
|
|
19064
|
+
function parseGrid(spec) {
|
|
19065
|
+
const m = /^(\d+)x(\d+)$/.exec(spec.trim());
|
|
19066
|
+
if (!m)
|
|
19067
|
+
throw new Error(`Invalid grid spec "${spec}" \u2014 expected RxC (e.g. 2x2, 3x1)`);
|
|
19068
|
+
const rows = parseInt(m[1], 10);
|
|
19069
|
+
const cols = parseInt(m[2], 10);
|
|
19070
|
+
if (rows < 1 || cols < 1) {
|
|
19071
|
+
throw new Error(`Invalid grid spec "${spec}" \u2014 rows and cols must be >= 1`);
|
|
19072
|
+
}
|
|
19073
|
+
return { rows, cols };
|
|
19074
|
+
}
|
|
19075
|
+
function parseTabsSpec(spec) {
|
|
19076
|
+
const parts = spec.split(",");
|
|
19077
|
+
if (parts.length === 0 || spec.trim() === "") {
|
|
19078
|
+
throw new Error(`Invalid tabs spec "${spec}" \u2014 expected comma-separated grids (e.g. "2x2,1x2")`);
|
|
19079
|
+
}
|
|
19080
|
+
return parts.map((p) => parseGrid(p));
|
|
19081
|
+
}
|
|
19082
|
+
function escapeAppleScript(s) {
|
|
19083
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
19084
|
+
}
|
|
19085
|
+
function shellQuote(s) {
|
|
19086
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19087
|
+
}
|
|
19088
|
+
function assignPaneCommands(totalPanes, run, all) {
|
|
19089
|
+
if (all) {
|
|
19090
|
+
if (run.length !== 1) {
|
|
19091
|
+
throw new Error(`--all requires exactly one --run command (got ${run.length})`);
|
|
19092
|
+
}
|
|
19093
|
+
return Array.from({ length: totalPanes }, () => run[0]);
|
|
19094
|
+
}
|
|
19095
|
+
if (run.length > totalPanes) {
|
|
19096
|
+
throw new Error(`Got ${run.length} commands for ${totalPanes} pane${totalPanes === 1 ? "" : "s"} \u2014 add panes or drop commands`);
|
|
19097
|
+
}
|
|
19098
|
+
return Array.from({ length: totalPanes }, (_, i) => run[i]);
|
|
19099
|
+
}
|
|
19100
|
+
function buildGhosttyScript(opts) {
|
|
19101
|
+
const { tabs, commands = [], dir, max = false } = opts;
|
|
19102
|
+
if (tabs.length === 0)
|
|
19103
|
+
throw new Error("At least one tab grid is required");
|
|
19104
|
+
const lines = [];
|
|
19105
|
+
lines.push('tell application "Ghostty"');
|
|
19106
|
+
lines.push(" activate");
|
|
19107
|
+
lines.push(" set w to new window");
|
|
19108
|
+
const term = (t, r, c) => `t${t}_${r}_${c}`;
|
|
19109
|
+
let paneIndex = 0;
|
|
19110
|
+
for (let t = 0;t < tabs.length; t++) {
|
|
19111
|
+
const { rows, cols } = tabs[t];
|
|
19112
|
+
if (t === 0) {
|
|
19113
|
+
lines.push(` set ${term(0, 0, 0)} to focused terminal of selected tab of w`);
|
|
19114
|
+
if (max) {
|
|
19115
|
+
lines.push(` perform action "toggle_maximize" on ${term(0, 0, 0)}`);
|
|
19116
|
+
}
|
|
19117
|
+
} else {
|
|
19118
|
+
lines.push(` set tb${t} to new tab in w`);
|
|
19119
|
+
lines.push(` set ${term(t, 0, 0)} to focused terminal of tb${t}`);
|
|
19120
|
+
}
|
|
19121
|
+
for (let r = 1;r < rows; r++) {
|
|
19122
|
+
lines.push(` set ${term(t, r, 0)} to split ${term(t, r - 1, 0)} direction down`);
|
|
19123
|
+
}
|
|
19124
|
+
for (let r = 0;r < rows; r++) {
|
|
19125
|
+
for (let c = 1;c < cols; c++) {
|
|
19126
|
+
lines.push(` set ${term(t, r, c)} to split ${term(t, r, c - 1)} direction right`);
|
|
19127
|
+
}
|
|
19128
|
+
}
|
|
19129
|
+
if (rows * cols > 1) {
|
|
19130
|
+
lines.push(` perform action "equalize_splits" on ${term(t, 0, 0)}`);
|
|
19131
|
+
}
|
|
19132
|
+
for (let r = 0;r < rows; r++) {
|
|
19133
|
+
for (let c = 0;c < cols; c++) {
|
|
19134
|
+
const raw = commands[paneIndex];
|
|
19135
|
+
paneIndex++;
|
|
19136
|
+
let cmd;
|
|
19137
|
+
if (dir && raw)
|
|
19138
|
+
cmd = `cd ${shellQuote(dir)} && ${raw}`;
|
|
19139
|
+
else if (dir)
|
|
19140
|
+
cmd = `cd ${shellQuote(dir)}`;
|
|
19141
|
+
else
|
|
19142
|
+
cmd = raw;
|
|
19143
|
+
if (cmd) {
|
|
19144
|
+
lines.push(` input text "${escapeAppleScript(cmd)}" to ${term(t, r, c)}`);
|
|
19145
|
+
lines.push(` send key "enter" to ${term(t, r, c)}`);
|
|
19146
|
+
}
|
|
19147
|
+
}
|
|
19148
|
+
}
|
|
19149
|
+
}
|
|
19150
|
+
lines.push(` focus ${term(0, 0, 0)}`);
|
|
19151
|
+
lines.push("end tell");
|
|
19152
|
+
return lines.join(`
|
|
19153
|
+
`) + `
|
|
19154
|
+
`;
|
|
19155
|
+
}
|
|
19156
|
+
|
|
19157
|
+
// src/apps/ghostty/driver.ts
|
|
19158
|
+
var APP_BUNDLE = "/Applications/Ghostty.app";
|
|
19159
|
+
function ghosttyAvailability(env = {}) {
|
|
19160
|
+
const platform = env.platform ?? process.platform;
|
|
19161
|
+
if (platform !== "darwin") {
|
|
19162
|
+
return { available: false, reason: "Ghostty orchestration requires macOS (AppleScript)" };
|
|
19163
|
+
}
|
|
19164
|
+
const hasAppBundle = env.hasAppBundle ?? existsSync4(APP_BUNDLE);
|
|
19165
|
+
const hasBinary = env.hasBinary ?? Bun.which("ghostty") !== null;
|
|
19166
|
+
if (!hasAppBundle && !hasBinary) {
|
|
19167
|
+
return { available: false, reason: `Ghostty not found (${APP_BUNDLE} missing and no \`ghostty\` on PATH)` };
|
|
19168
|
+
}
|
|
19169
|
+
return { available: true };
|
|
19170
|
+
}
|
|
19171
|
+
async function runOsascript(script) {
|
|
19172
|
+
const proc = Bun.spawn(["osascript", "-e", script], {
|
|
19173
|
+
stdout: "pipe",
|
|
19174
|
+
stderr: "pipe"
|
|
19175
|
+
});
|
|
19176
|
+
const code = await proc.exited;
|
|
19177
|
+
const stderr = await new Response(proc.stderr).text();
|
|
19178
|
+
return { ok: code === 0, stderr: stderr.trim() };
|
|
19179
|
+
}
|
|
19180
|
+
function resolveTabs(spec) {
|
|
19181
|
+
if (spec.tabs && spec.tabs.length > 0)
|
|
19182
|
+
return spec.tabs;
|
|
19183
|
+
return [spec.grid ?? { rows: 1, cols: 1 }];
|
|
19184
|
+
}
|
|
19185
|
+
var ghosttyDriver = {
|
|
19186
|
+
name: "ghostty",
|
|
19187
|
+
description: "Ghostty terminal \u2014 windows with pane grids, tabs, and a command per pane",
|
|
19188
|
+
available() {
|
|
19189
|
+
return ghosttyAvailability();
|
|
19190
|
+
},
|
|
19191
|
+
async open(spec) {
|
|
19192
|
+
const availability = ghosttyAvailability();
|
|
19193
|
+
if (!availability.available) {
|
|
19194
|
+
return { ok: false, message: availability.reason ?? "Ghostty is not available" };
|
|
19195
|
+
}
|
|
19196
|
+
const tabs = resolveTabs(spec);
|
|
19197
|
+
const totalPanes = tabs.reduce((sum, g) => sum + g.rows * g.cols, 0);
|
|
19198
|
+
let commands;
|
|
19199
|
+
try {
|
|
19200
|
+
commands = assignPaneCommands(totalPanes, spec.run ?? [], spec.all ?? false);
|
|
19201
|
+
} catch (err) {
|
|
19202
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
19203
|
+
}
|
|
19204
|
+
const script = buildGhosttyScript({
|
|
19205
|
+
tabs,
|
|
19206
|
+
commands,
|
|
19207
|
+
dir: spec.dir,
|
|
19208
|
+
max: spec.max
|
|
19209
|
+
});
|
|
19210
|
+
const result = await runOsascript(script);
|
|
19211
|
+
if (!result.ok) {
|
|
19212
|
+
return {
|
|
19213
|
+
ok: false,
|
|
19214
|
+
message: `osascript failed: ${result.stderr || "unknown error"}`,
|
|
19215
|
+
panes: totalPanes,
|
|
19216
|
+
tabs: tabs.length
|
|
19217
|
+
};
|
|
19218
|
+
}
|
|
19219
|
+
const tabDesc = tabs.map((g) => `${g.rows}x${g.cols}`).join(",");
|
|
19220
|
+
return {
|
|
19221
|
+
ok: true,
|
|
19222
|
+
message: `Opened Ghostty: ${tabs.length} tab${tabs.length === 1 ? "" : "s"} (${tabDesc}), ${totalPanes} pane${totalPanes === 1 ? "" : "s"}`,
|
|
19223
|
+
panes: totalPanes,
|
|
19224
|
+
tabs: tabs.length
|
|
19225
|
+
};
|
|
19226
|
+
}
|
|
19227
|
+
};
|
|
19228
|
+
|
|
19229
|
+
// src/apps/registry.ts
|
|
19230
|
+
var drivers = new Map;
|
|
19231
|
+
function registerAppDriver(driver) {
|
|
19232
|
+
drivers.set(driver.name.toLowerCase(), driver);
|
|
19233
|
+
}
|
|
19234
|
+
function getAppDriver(name) {
|
|
19235
|
+
return drivers.get(name.toLowerCase());
|
|
19236
|
+
}
|
|
19237
|
+
function listAppDrivers() {
|
|
19238
|
+
return [...drivers.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
19239
|
+
}
|
|
19240
|
+
registerAppDriver(ghosttyDriver);
|
|
19241
|
+
|
|
19058
19242
|
// src/db/pg-migrations.ts
|
|
19059
19243
|
var PG_MIGRATIONS = [
|
|
19060
19244
|
`
|
|
@@ -19164,32 +19348,65 @@ class PgAdapterAsync {
|
|
|
19164
19348
|
}
|
|
19165
19349
|
}
|
|
19166
19350
|
|
|
19167
|
-
// src/db/
|
|
19168
|
-
var
|
|
19351
|
+
// src/db/storage-sync.ts
|
|
19352
|
+
var STORAGE_TABLES = ["sessions", "action_logs", "feedback"];
|
|
19353
|
+
var COMPUTER_STORAGE_ENV = "HASNA_COMPUTER_DATABASE_URL";
|
|
19354
|
+
var COMPUTER_STORAGE_FALLBACK_ENV = "COMPUTER_DATABASE_URL";
|
|
19355
|
+
var COMPUTER_STORAGE_MODE_ENV = "HASNA_COMPUTER_STORAGE_MODE";
|
|
19356
|
+
var COMPUTER_STORAGE_MODE_FALLBACK_ENV = "COMPUTER_STORAGE_MODE";
|
|
19357
|
+
var STORAGE_DATABASE_ENV = [COMPUTER_STORAGE_ENV, COMPUTER_STORAGE_FALLBACK_ENV];
|
|
19169
19358
|
var PRIMARY_KEYS = {
|
|
19170
19359
|
sessions: ["id"],
|
|
19171
19360
|
action_logs: ["id"],
|
|
19172
19361
|
feedback: ["id"]
|
|
19173
19362
|
};
|
|
19174
|
-
function
|
|
19175
|
-
|
|
19363
|
+
function readEnv3(name) {
|
|
19364
|
+
const value = process.env[name]?.trim();
|
|
19365
|
+
return value || undefined;
|
|
19366
|
+
}
|
|
19367
|
+
function normalizeStorageMode(value) {
|
|
19368
|
+
const normalized = value?.trim().toLowerCase();
|
|
19369
|
+
if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
|
|
19370
|
+
return normalized;
|
|
19371
|
+
return;
|
|
19372
|
+
}
|
|
19373
|
+
function getStorageDatabaseEnvName() {
|
|
19374
|
+
for (const name of STORAGE_DATABASE_ENV) {
|
|
19375
|
+
if (readEnv3(name))
|
|
19376
|
+
return name;
|
|
19377
|
+
}
|
|
19378
|
+
return null;
|
|
19379
|
+
}
|
|
19380
|
+
function getStorageDatabaseEnv() {
|
|
19381
|
+
const name = getStorageDatabaseEnvName();
|
|
19382
|
+
return name ? { name } : null;
|
|
19176
19383
|
}
|
|
19177
|
-
|
|
19178
|
-
const
|
|
19384
|
+
function getStorageDatabaseUrl() {
|
|
19385
|
+
const env = getStorageDatabaseEnv();
|
|
19386
|
+
return env ? readEnv3(env.name) ?? null : null;
|
|
19387
|
+
}
|
|
19388
|
+
function getStorageMode() {
|
|
19389
|
+
const mode = normalizeStorageMode(readEnv3(COMPUTER_STORAGE_MODE_ENV) ?? readEnv3(COMPUTER_STORAGE_MODE_FALLBACK_ENV));
|
|
19390
|
+
if (mode)
|
|
19391
|
+
return mode;
|
|
19392
|
+
return getStorageDatabaseUrl() ? "hybrid" : "local";
|
|
19393
|
+
}
|
|
19394
|
+
async function getStoragePg() {
|
|
19395
|
+
const url = getStorageDatabaseUrl();
|
|
19179
19396
|
if (!url) {
|
|
19180
|
-
throw new Error("Missing
|
|
19397
|
+
throw new Error("Missing HASNA_COMPUTER_DATABASE_URL");
|
|
19181
19398
|
}
|
|
19182
19399
|
return new PgAdapterAsync(url);
|
|
19183
19400
|
}
|
|
19184
|
-
async function
|
|
19401
|
+
async function runStorageMigrations(remote) {
|
|
19185
19402
|
for (const sql of PG_MIGRATIONS)
|
|
19186
19403
|
await remote.run(sql);
|
|
19187
19404
|
}
|
|
19188
|
-
async function
|
|
19189
|
-
const remote = await
|
|
19405
|
+
async function storagePush(options) {
|
|
19406
|
+
const remote = await getStoragePg();
|
|
19190
19407
|
const db2 = getDb();
|
|
19191
19408
|
try {
|
|
19192
|
-
await
|
|
19409
|
+
await runStorageMigrations(remote);
|
|
19193
19410
|
const results = [];
|
|
19194
19411
|
for (const table of resolveTables(options?.tables))
|
|
19195
19412
|
results.push(await pushTable(db2, remote, table));
|
|
@@ -19199,11 +19416,11 @@ async function cloudPush(options) {
|
|
|
19199
19416
|
await remote.close();
|
|
19200
19417
|
}
|
|
19201
19418
|
}
|
|
19202
|
-
async function
|
|
19203
|
-
const remote = await
|
|
19419
|
+
async function storagePull(options) {
|
|
19420
|
+
const remote = await getStoragePg();
|
|
19204
19421
|
const db2 = getDb();
|
|
19205
19422
|
try {
|
|
19206
|
-
await
|
|
19423
|
+
await runStorageMigrations(remote);
|
|
19207
19424
|
const results = [];
|
|
19208
19425
|
for (const table of resolveTables(options?.tables))
|
|
19209
19426
|
results.push(await pullTable(remote, db2, table));
|
|
@@ -19213,9 +19430,9 @@ async function cloudPull(options) {
|
|
|
19213
19430
|
await remote.close();
|
|
19214
19431
|
}
|
|
19215
19432
|
}
|
|
19216
|
-
async function
|
|
19217
|
-
const pull = await
|
|
19218
|
-
const push2 = await
|
|
19433
|
+
async function storageSync(options) {
|
|
19434
|
+
const pull = await storagePull(options);
|
|
19435
|
+
const push2 = await storagePush(options);
|
|
19219
19436
|
return { pull, push: push2 };
|
|
19220
19437
|
}
|
|
19221
19438
|
function getSyncMetaAll() {
|
|
@@ -19223,10 +19440,22 @@ function getSyncMetaAll() {
|
|
|
19223
19440
|
ensureSyncMetaTable(db2);
|
|
19224
19441
|
return db2.prepare("SELECT table_name, last_synced_at, direction FROM _computer_sync_meta ORDER BY table_name, direction").all();
|
|
19225
19442
|
}
|
|
19443
|
+
function getStorageStatus() {
|
|
19444
|
+
const activeEnv = getStorageDatabaseEnv();
|
|
19445
|
+
return {
|
|
19446
|
+
configured: Boolean(activeEnv),
|
|
19447
|
+
mode: getStorageMode(),
|
|
19448
|
+
env: STORAGE_DATABASE_ENV,
|
|
19449
|
+
activeEnv: activeEnv?.name ?? null,
|
|
19450
|
+
service: "computer",
|
|
19451
|
+
tables: STORAGE_TABLES,
|
|
19452
|
+
sync: getSyncMetaAll()
|
|
19453
|
+
};
|
|
19454
|
+
}
|
|
19226
19455
|
function resolveTables(tables) {
|
|
19227
19456
|
if (!tables || tables.length === 0)
|
|
19228
|
-
return [...
|
|
19229
|
-
const allowed = new Set(
|
|
19457
|
+
return [...STORAGE_TABLES];
|
|
19458
|
+
const allowed = new Set(STORAGE_TABLES);
|
|
19230
19459
|
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
19231
19460
|
const invalid = requested.filter((table) => !allowed.has(table));
|
|
19232
19461
|
if (invalid.length > 0)
|
|
@@ -19352,256 +19581,374 @@ function coerceForSqlite(value) {
|
|
|
19352
19581
|
return String(value);
|
|
19353
19582
|
}
|
|
19354
19583
|
|
|
19355
|
-
// src/mcp/
|
|
19356
|
-
|
|
19357
|
-
name: "computer",
|
|
19358
|
-
version: "0.1.0"
|
|
19359
|
-
});
|
|
19360
|
-
function cloudResult(value) {
|
|
19584
|
+
// src/mcp/server.ts
|
|
19585
|
+
function storageResult(value) {
|
|
19361
19586
|
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
19362
19587
|
}
|
|
19363
|
-
|
|
19364
|
-
|
|
19365
|
-
|
|
19366
|
-
|
|
19367
|
-
max_steps: exports_external.number().default(50).describe("Maximum steps before stopping"),
|
|
19368
|
-
save_screenshots: exports_external.boolean().default(false).describe("Save screenshots to disk"),
|
|
19369
|
-
dry_run: exports_external.boolean().default(false).describe("Plan actions without executing them")
|
|
19370
|
-
}, async (params) => {
|
|
19371
|
-
const session = await runTask({
|
|
19372
|
-
task: params.task,
|
|
19373
|
-
provider: params.provider,
|
|
19374
|
-
model: params.model,
|
|
19375
|
-
maxSteps: params.max_steps,
|
|
19376
|
-
saveScreenshots: params.save_screenshots,
|
|
19377
|
-
dryRun: params.dry_run
|
|
19588
|
+
function buildServer() {
|
|
19589
|
+
const server = new McpServer({
|
|
19590
|
+
name: "computer",
|
|
19591
|
+
version: "0.1.10"
|
|
19378
19592
|
});
|
|
19379
|
-
|
|
19380
|
-
|
|
19381
|
-
|
|
19382
|
-
|
|
19383
|
-
|
|
19384
|
-
|
|
19385
|
-
|
|
19386
|
-
}
|
|
19387
|
-
|
|
19388
|
-
|
|
19389
|
-
|
|
19390
|
-
|
|
19391
|
-
|
|
19392
|
-
|
|
19393
|
-
|
|
19394
|
-
|
|
19395
|
-
|
|
19396
|
-
|
|
19397
|
-
|
|
19398
|
-
|
|
19399
|
-
|
|
19400
|
-
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19405
|
-
|
|
19406
|
-
|
|
19593
|
+
server.tool("computer_run_task", "Run a computer use task \u2014 the AI sees your screen and controls mouse/keyboard to complete it", {
|
|
19594
|
+
task: exports_external.string().describe("Natural language description of what to do"),
|
|
19595
|
+
provider: exports_external.enum(["anthropic", "openai"]).default("anthropic").describe("AI provider"),
|
|
19596
|
+
model: exports_external.string().optional().describe("Specific model to use"),
|
|
19597
|
+
max_steps: exports_external.number().default(50).describe("Maximum steps before stopping"),
|
|
19598
|
+
save_screenshots: exports_external.boolean().default(false).describe("Save screenshots to disk"),
|
|
19599
|
+
dry_run: exports_external.boolean().default(false).describe("Plan actions without executing them")
|
|
19600
|
+
}, async (params) => {
|
|
19601
|
+
const session = await runTask({
|
|
19602
|
+
task: params.task,
|
|
19603
|
+
provider: params.provider,
|
|
19604
|
+
model: params.model,
|
|
19605
|
+
maxSteps: params.max_steps,
|
|
19606
|
+
saveScreenshots: params.save_screenshots,
|
|
19607
|
+
dryRun: params.dry_run
|
|
19608
|
+
});
|
|
19609
|
+
return {
|
|
19610
|
+
content: [
|
|
19611
|
+
{
|
|
19612
|
+
type: "text",
|
|
19613
|
+
text: JSON.stringify(session, null, 2)
|
|
19614
|
+
}
|
|
19615
|
+
]
|
|
19616
|
+
};
|
|
19617
|
+
});
|
|
19618
|
+
server.tool("computer_screenshot", "Capture a screenshot of the current screen", {
|
|
19619
|
+
save_to: exports_external.string().optional().describe("Optional file path to save the screenshot")
|
|
19620
|
+
}, async (params) => {
|
|
19621
|
+
const ss = await captureScreenshot();
|
|
19622
|
+
if (params.save_to) {
|
|
19623
|
+
const dir = params.save_to.substring(0, params.save_to.lastIndexOf("/"));
|
|
19624
|
+
const file = params.save_to.substring(params.save_to.lastIndexOf("/") + 1);
|
|
19625
|
+
await saveScreenshotToFile(ss, dir, file);
|
|
19626
|
+
}
|
|
19627
|
+
return {
|
|
19628
|
+
content: [
|
|
19629
|
+
{
|
|
19630
|
+
type: "image",
|
|
19631
|
+
data: ss.base64,
|
|
19632
|
+
mimeType: "image/png"
|
|
19633
|
+
},
|
|
19634
|
+
{
|
|
19635
|
+
type: "text",
|
|
19636
|
+
text: `Screen: ${ss.size.width}x${ss.size.height}`
|
|
19637
|
+
}
|
|
19638
|
+
]
|
|
19639
|
+
};
|
|
19640
|
+
});
|
|
19641
|
+
server.tool("computer_click", "Click at a specific screen coordinate", {
|
|
19642
|
+
x: exports_external.number().describe("X coordinate"),
|
|
19643
|
+
y: exports_external.number().describe("Y coordinate"),
|
|
19644
|
+
button: exports_external.enum(["left", "right", "middle"]).default("left").describe("Mouse button"),
|
|
19645
|
+
count: exports_external.number().default(1).describe("Click count (1=single, 2=double, 3=triple)")
|
|
19646
|
+
}, async (params) => {
|
|
19647
|
+
const result = await executeAction({
|
|
19648
|
+
type: "click",
|
|
19649
|
+
point: { x: params.x, y: params.y },
|
|
19650
|
+
button: params.button,
|
|
19651
|
+
count: params.count
|
|
19652
|
+
});
|
|
19653
|
+
const content = [{ type: "text", text: result.success ? "Click executed" : `Click failed: ${result.error}` }];
|
|
19654
|
+
if (result.screenshot) {
|
|
19655
|
+
content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
|
|
19656
|
+
}
|
|
19657
|
+
return { content };
|
|
19658
|
+
});
|
|
19659
|
+
server.tool("computer_type", "Type text using the keyboard", {
|
|
19660
|
+
text: exports_external.string().describe("Text to type")
|
|
19661
|
+
}, async (params) => {
|
|
19662
|
+
const result = await executeAction({ type: "type", text: params.text });
|
|
19663
|
+
const content = [{ type: "text", text: result.success ? "Text typed" : `Type failed: ${result.error}` }];
|
|
19664
|
+
if (result.screenshot) {
|
|
19665
|
+
content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
|
|
19666
|
+
}
|
|
19667
|
+
return { content };
|
|
19668
|
+
});
|
|
19669
|
+
server.tool("computer_key", "Press a key or key combination (e.g. 'enter', 'cmd+c', 'ctrl+shift+a')", {
|
|
19670
|
+
keys: exports_external.string().describe("Key or combination to press")
|
|
19671
|
+
}, async (params) => {
|
|
19672
|
+
const result = await executeAction({ type: "key", keys: params.keys });
|
|
19673
|
+
const content = [{ type: "text", text: result.success ? "Key pressed" : `Key failed: ${result.error}` }];
|
|
19674
|
+
if (result.screenshot) {
|
|
19675
|
+
content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
|
|
19676
|
+
}
|
|
19677
|
+
return { content };
|
|
19678
|
+
});
|
|
19679
|
+
server.tool("computer_scroll", "Scroll at a specific position", {
|
|
19680
|
+
x: exports_external.number().describe("X coordinate"),
|
|
19681
|
+
y: exports_external.number().describe("Y coordinate"),
|
|
19682
|
+
direction: exports_external.enum(["up", "down"]).describe("Scroll direction"),
|
|
19683
|
+
amount: exports_external.number().default(3).describe("Scroll amount")
|
|
19684
|
+
}, async (params) => {
|
|
19685
|
+
const dy = params.direction === "down" ? params.amount : -params.amount;
|
|
19686
|
+
const result = await executeAction({
|
|
19687
|
+
type: "scroll",
|
|
19688
|
+
point: { x: params.x, y: params.y },
|
|
19689
|
+
deltaX: 0,
|
|
19690
|
+
deltaY: dy
|
|
19691
|
+
});
|
|
19692
|
+
const content = [{ type: "text", text: result.success ? "Scrolled" : `Scroll failed: ${result.error}` }];
|
|
19693
|
+
if (result.screenshot) {
|
|
19694
|
+
content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
|
|
19695
|
+
}
|
|
19696
|
+
return { content };
|
|
19697
|
+
});
|
|
19698
|
+
server.tool("computer_mouse_move", "Move the mouse to a position", {
|
|
19699
|
+
x: exports_external.number().describe("X coordinate"),
|
|
19700
|
+
y: exports_external.number().describe("Y coordinate")
|
|
19701
|
+
}, async (params) => {
|
|
19702
|
+
const result = await executeAction({ type: "mouse_move", point: { x: params.x, y: params.y } });
|
|
19703
|
+
return { content: [{ type: "text", text: result.success ? "Mouse moved" : `Move failed: ${result.error}` }] };
|
|
19704
|
+
});
|
|
19705
|
+
server.tool("computer_open_url", "Open a URL in the default browser", {
|
|
19706
|
+
url: exports_external.string().describe("URL to open")
|
|
19707
|
+
}, async (params) => {
|
|
19708
|
+
const result = await executeAction({ type: "open_url", url: params.url });
|
|
19709
|
+
const content = [{ type: "text", text: result.success ? "URL opened" : `Open failed: ${result.error}` }];
|
|
19710
|
+
if (result.screenshot) {
|
|
19711
|
+
content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
|
|
19712
|
+
}
|
|
19713
|
+
return { content };
|
|
19714
|
+
});
|
|
19715
|
+
server.tool("computer_open_app", "Open a macOS application. Apps with a registered driver (see computer_list_apps) support deterministic orchestration: pane grids, multiple tabs, a command per pane, working directory, and maximize. Other apps open normally.", {
|
|
19716
|
+
app: exports_external.string().optional().describe("Application or driver name (e.g. 'ghostty', 'Safari', 'Slack')"),
|
|
19717
|
+
name: exports_external.string().optional().describe("Deprecated alias for `app`"),
|
|
19718
|
+
grid: exports_external.string().optional().describe('Pane grid RxC, e.g. "2x2" (driver apps only)'),
|
|
19719
|
+
tabs: exports_external.string().optional().describe('Comma-separated grid specs, one per tab, e.g. "2x2,1x2,1x2" (driver apps only)'),
|
|
19720
|
+
run: exports_external.array(exports_external.string()).optional().describe("Commands per pane in row-major order across tabs (driver apps only)"),
|
|
19721
|
+
all: exports_external.boolean().default(false).describe("Run the single `run` command in every pane"),
|
|
19722
|
+
dir: exports_external.string().optional().describe("Working directory \u2014 every pane cds here first"),
|
|
19723
|
+
max: exports_external.boolean().default(false).describe("Maximize the new window (not native fullscreen)")
|
|
19724
|
+
}, async (params) => {
|
|
19725
|
+
const appName = params.app ?? params.name;
|
|
19726
|
+
if (!appName) {
|
|
19727
|
+
return { content: [{ type: "text", text: "Missing required parameter: app" }] };
|
|
19728
|
+
}
|
|
19729
|
+
const driver = getAppDriver(appName);
|
|
19730
|
+
if (driver) {
|
|
19731
|
+
try {
|
|
19732
|
+
const result2 = await driver.open({
|
|
19733
|
+
grid: params.grid ? parseGrid(params.grid) : undefined,
|
|
19734
|
+
tabs: params.tabs ? parseTabsSpec(params.tabs) : undefined,
|
|
19735
|
+
run: params.run,
|
|
19736
|
+
all: params.all,
|
|
19737
|
+
dir: params.dir,
|
|
19738
|
+
max: params.max
|
|
19739
|
+
});
|
|
19740
|
+
return { content: [{ type: "text", text: result2.message }] };
|
|
19741
|
+
} catch (err) {
|
|
19742
|
+
return { content: [{ type: "text", text: `Open failed: ${err instanceof Error ? err.message : err}` }] };
|
|
19407
19743
|
}
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
|
|
19411
|
-
|
|
19412
|
-
|
|
19413
|
-
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19744
|
+
}
|
|
19745
|
+
if (params.grid || params.tabs || params.run?.length || params.dir) {
|
|
19746
|
+
return {
|
|
19747
|
+
content: [
|
|
19748
|
+
{ type: "text", text: `No app driver registered for "${appName}" \u2014 grid/tabs/run/dir need a driver (see computer_list_apps). Opening normally requires dropping those params.` }
|
|
19749
|
+
]
|
|
19750
|
+
};
|
|
19751
|
+
}
|
|
19752
|
+
const result = await executeAction({ type: "open_app", name: appName });
|
|
19753
|
+
const content = [{ type: "text", text: result.success ? `Opened ${appName}` : `Open failed: ${result.error}` }];
|
|
19754
|
+
if (result.screenshot) {
|
|
19755
|
+
content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
|
|
19756
|
+
}
|
|
19757
|
+
return { content };
|
|
19758
|
+
});
|
|
19759
|
+
server.tool("computer_list_apps", "List registered app drivers (deterministic orchestration) and their availability on this machine", {}, async () => {
|
|
19760
|
+
const apps = listAppDrivers().map((driver) => {
|
|
19761
|
+
const availability = driver.available();
|
|
19762
|
+
return {
|
|
19763
|
+
name: driver.name,
|
|
19764
|
+
description: driver.description,
|
|
19765
|
+
available: availability.available,
|
|
19766
|
+
...availability.reason ? { reason: availability.reason } : {}
|
|
19767
|
+
};
|
|
19768
|
+
});
|
|
19769
|
+
return { content: [{ type: "text", text: JSON.stringify(apps, null, 2) }] };
|
|
19770
|
+
});
|
|
19771
|
+
server.tool("computer_screen_size", "Get the current screen resolution", {}, async () => {
|
|
19772
|
+
const size = await getScreenSize();
|
|
19773
|
+
return { content: [{ type: "text", text: `${size.width}x${size.height}` }] };
|
|
19774
|
+
});
|
|
19775
|
+
server.tool("computer_list_sessions", "List past computer use sessions", {
|
|
19776
|
+
limit: exports_external.number().default(20).describe("Max sessions to return"),
|
|
19777
|
+
status: exports_external.enum(["running", "paused", "completed", "failed", "cancelled"]).optional().describe("Filter by status")
|
|
19778
|
+
}, async (params) => {
|
|
19779
|
+
const sessions = listSessions({ limit: params.limit, status: params.status });
|
|
19780
|
+
return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
|
|
19781
|
+
});
|
|
19782
|
+
server.tool("computer_get_session", "Get details of a specific session including action log", {
|
|
19783
|
+
id: exports_external.string().describe("Session ID")
|
|
19784
|
+
}, async (params) => {
|
|
19785
|
+
const session = getSession(params.id);
|
|
19786
|
+
if (!session)
|
|
19787
|
+
return { content: [{ type: "text", text: "Session not found" }] };
|
|
19788
|
+
const logs = getActionLogs(params.id);
|
|
19789
|
+
return {
|
|
19790
|
+
content: [
|
|
19791
|
+
{ type: "text", text: JSON.stringify({ session, action_logs: logs }, null, 2) }
|
|
19792
|
+
]
|
|
19793
|
+
};
|
|
19794
|
+
});
|
|
19795
|
+
server.tool("computer_delete_session", "Delete a session and its action logs", {
|
|
19796
|
+
id: exports_external.string().describe("Session ID")
|
|
19797
|
+
}, async (params) => {
|
|
19798
|
+
const deleted = deleteSession(params.id);
|
|
19799
|
+
return { content: [{ type: "text", text: deleted ? "Session deleted" : "Session not found" }] };
|
|
19800
|
+
});
|
|
19801
|
+
server.tool("computer_search", "Full-text search across sessions (by task) and action logs (by reasoning)", {
|
|
19802
|
+
query: exports_external.string().describe("Search query"),
|
|
19803
|
+
scope: exports_external.enum(["sessions", "actions", "both"]).default("both").describe("Where to search"),
|
|
19804
|
+
limit: exports_external.number().default(20).describe("Max results")
|
|
19805
|
+
}, async (params) => {
|
|
19806
|
+
const results = {};
|
|
19807
|
+
if (params.scope === "sessions" || params.scope === "both") {
|
|
19808
|
+
results.sessions = searchSessions(params.query, params.limit);
|
|
19809
|
+
}
|
|
19810
|
+
if (params.scope === "actions" || params.scope === "both") {
|
|
19811
|
+
results.action_logs = searchActionLogs(params.query, params.limit);
|
|
19812
|
+
}
|
|
19813
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
19814
|
+
});
|
|
19815
|
+
server.tool("computer_stats", "Get usage statistics for computer use", {}, async () => {
|
|
19816
|
+
const stats = getStats();
|
|
19817
|
+
return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
|
|
19422
19818
|
});
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
19819
|
+
server.tool("computer_accessibility", "Query the macOS accessibility tree \u2014 get structured UI elements (buttons, fields, labels) with positions. Much more precise than pixel-guessing from screenshots.", {
|
|
19820
|
+
app: exports_external.string().optional().describe("App name to query (default: frontmost)"),
|
|
19821
|
+
focused_only: exports_external.boolean().default(false).describe("Only get focused element's subtree"),
|
|
19822
|
+
depth: exports_external.number().default(3).describe("Max tree traversal depth"),
|
|
19823
|
+
format: exports_external.enum(["json", "summary"]).default("summary").describe("Output format")
|
|
19824
|
+
}, async (params) => {
|
|
19825
|
+
try {
|
|
19826
|
+
const elements = await queryAccessibilityTree({
|
|
19827
|
+
app: params.app,
|
|
19828
|
+
focusedOnly: params.focused_only,
|
|
19829
|
+
depth: params.depth
|
|
19830
|
+
});
|
|
19831
|
+
const text = params.format === "json" ? JSON.stringify(elements, null, 2) : summarizeAccessibilityTree(elements);
|
|
19832
|
+
return { content: [{ type: "text", text }] };
|
|
19833
|
+
} catch (err) {
|
|
19834
|
+
return { content: [{ type: "text", text: `Accessibility query failed: ${err instanceof Error ? err.message : err}` }] };
|
|
19835
|
+
}
|
|
19836
|
+
});
|
|
19837
|
+
server.tool("computer_register_agent", "Register an agent for multi-agent coordination", {
|
|
19838
|
+
name: exports_external.string().describe("Agent name"),
|
|
19839
|
+
description: exports_external.string().optional().describe("Agent description"),
|
|
19840
|
+
capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities")
|
|
19841
|
+
}, async (params) => {
|
|
19842
|
+
const agent = registerAgent(params);
|
|
19843
|
+
return { content: [{ type: "text", text: JSON.stringify(agent, null, 2) }] };
|
|
19844
|
+
});
|
|
19845
|
+
server.tool("computer_heartbeat", "Send a heartbeat to mark an agent as active", {
|
|
19846
|
+
agent_id: exports_external.string().describe("Agent ID")
|
|
19847
|
+
}, async (params) => {
|
|
19848
|
+
const ok = heartbeat(params.agent_id);
|
|
19849
|
+
return { content: [{ type: "text", text: ok ? "Heartbeat received" : "Agent not found" }] };
|
|
19850
|
+
});
|
|
19851
|
+
server.tool("computer_set_focus", "Set what an agent is currently focused on", {
|
|
19852
|
+
agent_id: exports_external.string().describe("Agent ID"),
|
|
19853
|
+
focus: exports_external.string().describe("Current focus description")
|
|
19854
|
+
}, async (params) => {
|
|
19855
|
+
const ok = setFocus(params.agent_id, params.focus);
|
|
19856
|
+
return { content: [{ type: "text", text: ok ? "Focus updated" : "Agent not found" }] };
|
|
19857
|
+
});
|
|
19858
|
+
server.tool("computer_list_agents", "List all registered agents", {}, async () => {
|
|
19859
|
+
const agents = listAgents();
|
|
19860
|
+
return { content: [{ type: "text", text: JSON.stringify(agents, null, 2) }] };
|
|
19861
|
+
});
|
|
19862
|
+
server.tool("storage_status", "Show computer storage sync configuration and local sync history", {}, async () => storageResult(getStorageStatus()));
|
|
19863
|
+
server.tool("storage_push", "Push local computer data to storage PostgreSQL", { tables: exports_external.array(exports_external.string()).optional() }, async ({ tables }) => storageResult(await storagePush(tables ? { tables } : undefined)));
|
|
19864
|
+
server.tool("storage_pull", "Pull computer data from storage PostgreSQL to local SQLite", { tables: exports_external.array(exports_external.string()).optional() }, async ({ tables }) => storageResult(await storagePull(tables ? { tables } : undefined)));
|
|
19865
|
+
server.tool("storage_sync", "Bidirectional computer sync: pull then push", { tables: exports_external.array(exports_external.string()).optional() }, async ({ tables }) => storageResult(await storageSync(tables ? { tables } : undefined)));
|
|
19866
|
+
return server;
|
|
19867
|
+
}
|
|
19868
|
+
|
|
19869
|
+
// src/mcp/http.ts
|
|
19870
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
19871
|
+
var MCP_HTTP_PORT = 8883;
|
|
19872
|
+
var MCP_NAME = "computer";
|
|
19873
|
+
function isStdioMode(argv) {
|
|
19874
|
+
return argv.includes("--stdio") || process.env.MCP_STDIO === "1";
|
|
19875
|
+
}
|
|
19876
|
+
function resolveHttpPort(argv) {
|
|
19877
|
+
const eqArg = argv.find((a) => a.startsWith("--port="));
|
|
19878
|
+
if (eqArg) {
|
|
19879
|
+
const parsed = Number.parseInt(eqArg.slice("--port=".length), 10);
|
|
19880
|
+
if (!Number.isNaN(parsed))
|
|
19881
|
+
return parsed;
|
|
19426
19882
|
}
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
|
|
19430
|
-
|
|
19431
|
-
|
|
19432
|
-
|
|
19433
|
-
const
|
|
19434
|
-
if (
|
|
19435
|
-
|
|
19436
|
-
|
|
19437
|
-
|
|
19438
|
-
}
|
|
19439
|
-
|
|
19440
|
-
|
|
19441
|
-
|
|
19442
|
-
|
|
19443
|
-
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
|
|
19447
|
-
|
|
19448
|
-
|
|
19449
|
-
server.tool("computer_scroll", "Scroll at a specific position", {
|
|
19450
|
-
x: exports_external.number().describe("X coordinate"),
|
|
19451
|
-
y: exports_external.number().describe("Y coordinate"),
|
|
19452
|
-
direction: exports_external.enum(["up", "down"]).describe("Scroll direction"),
|
|
19453
|
-
amount: exports_external.number().default(3).describe("Scroll amount")
|
|
19454
|
-
}, async (params) => {
|
|
19455
|
-
const dy = params.direction === "down" ? params.amount : -params.amount;
|
|
19456
|
-
const result = await executeAction({
|
|
19457
|
-
type: "scroll",
|
|
19458
|
-
point: { x: params.x, y: params.y },
|
|
19459
|
-
deltaX: 0,
|
|
19460
|
-
deltaY: dy
|
|
19883
|
+
const idx = argv.indexOf("--port");
|
|
19884
|
+
if (idx >= 0) {
|
|
19885
|
+
const parsed = Number.parseInt(argv[idx + 1] ?? "", 10);
|
|
19886
|
+
if (!Number.isNaN(parsed))
|
|
19887
|
+
return parsed;
|
|
19888
|
+
}
|
|
19889
|
+
const envPort = process.env.MCP_HTTP_PORT;
|
|
19890
|
+
if (envPort) {
|
|
19891
|
+
const parsed = Number.parseInt(envPort, 10);
|
|
19892
|
+
if (!Number.isNaN(parsed))
|
|
19893
|
+
return parsed;
|
|
19894
|
+
}
|
|
19895
|
+
return MCP_HTTP_PORT;
|
|
19896
|
+
}
|
|
19897
|
+
function healthPayload() {
|
|
19898
|
+
return { status: "ok", name: MCP_NAME };
|
|
19899
|
+
}
|
|
19900
|
+
async function handleMcpRequest(req) {
|
|
19901
|
+
const server = buildServer();
|
|
19902
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
19903
|
+
sessionIdGenerator: undefined,
|
|
19904
|
+
enableJsonResponse: true
|
|
19461
19905
|
});
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19906
|
+
await server.connect(transport);
|
|
19907
|
+
return transport.handleRequest(req);
|
|
19908
|
+
}
|
|
19909
|
+
async function handleMcpHttpRequest(req) {
|
|
19910
|
+
const url = new URL(req.url);
|
|
19911
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
19912
|
+
return Response.json(healthPayload());
|
|
19465
19913
|
}
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
server.tool("computer_mouse_move", "Move the mouse to a position", {
|
|
19469
|
-
x: exports_external.number().describe("X coordinate"),
|
|
19470
|
-
y: exports_external.number().describe("Y coordinate")
|
|
19471
|
-
}, async (params) => {
|
|
19472
|
-
const result = await executeAction({ type: "mouse_move", point: { x: params.x, y: params.y } });
|
|
19473
|
-
return { content: [{ type: "text", text: result.success ? "Mouse moved" : `Move failed: ${result.error}` }] };
|
|
19474
|
-
});
|
|
19475
|
-
server.tool("computer_open_url", "Open a URL in the default browser", {
|
|
19476
|
-
url: exports_external.string().describe("URL to open")
|
|
19477
|
-
}, async (params) => {
|
|
19478
|
-
const result = await executeAction({ type: "open_url", url: params.url });
|
|
19479
|
-
const content = [{ type: "text", text: result.success ? "URL opened" : `Open failed: ${result.error}` }];
|
|
19480
|
-
if (result.screenshot) {
|
|
19481
|
-
content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
|
|
19482
|
-
}
|
|
19483
|
-
return { content };
|
|
19484
|
-
});
|
|
19485
|
-
server.tool("computer_open_app", "Open a macOS application by name", {
|
|
19486
|
-
name: exports_external.string().describe("Application name (e.g. 'Safari', 'Terminal', 'Slack')")
|
|
19487
|
-
}, async (params) => {
|
|
19488
|
-
const result = await executeAction({ type: "open_app", name: params.name });
|
|
19489
|
-
const content = [{ type: "text", text: result.success ? `Opened ${params.name}` : `Open failed: ${result.error}` }];
|
|
19490
|
-
if (result.screenshot) {
|
|
19491
|
-
content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
|
|
19492
|
-
}
|
|
19493
|
-
return { content };
|
|
19494
|
-
});
|
|
19495
|
-
server.tool("computer_screen_size", "Get the current screen resolution", {}, async () => {
|
|
19496
|
-
const size = await getScreenSize();
|
|
19497
|
-
return { content: [{ type: "text", text: `${size.width}x${size.height}` }] };
|
|
19498
|
-
});
|
|
19499
|
-
server.tool("computer_list_sessions", "List past computer use sessions", {
|
|
19500
|
-
limit: exports_external.number().default(20).describe("Max sessions to return"),
|
|
19501
|
-
status: exports_external.enum(["running", "paused", "completed", "failed", "cancelled"]).optional().describe("Filter by status")
|
|
19502
|
-
}, async (params) => {
|
|
19503
|
-
const sessions = listSessions({ limit: params.limit, status: params.status });
|
|
19504
|
-
return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
|
|
19505
|
-
});
|
|
19506
|
-
server.tool("computer_get_session", "Get details of a specific session including action log", {
|
|
19507
|
-
id: exports_external.string().describe("Session ID")
|
|
19508
|
-
}, async (params) => {
|
|
19509
|
-
const session = getSession(params.id);
|
|
19510
|
-
if (!session)
|
|
19511
|
-
return { content: [{ type: "text", text: "Session not found" }] };
|
|
19512
|
-
const logs = getActionLogs(params.id);
|
|
19513
|
-
return {
|
|
19514
|
-
content: [
|
|
19515
|
-
{ type: "text", text: JSON.stringify({ session, action_logs: logs }, null, 2) }
|
|
19516
|
-
]
|
|
19517
|
-
};
|
|
19518
|
-
});
|
|
19519
|
-
server.tool("computer_delete_session", "Delete a session and its action logs", {
|
|
19520
|
-
id: exports_external.string().describe("Session ID")
|
|
19521
|
-
}, async (params) => {
|
|
19522
|
-
const deleted = deleteSession(params.id);
|
|
19523
|
-
return { content: [{ type: "text", text: deleted ? "Session deleted" : "Session not found" }] };
|
|
19524
|
-
});
|
|
19525
|
-
server.tool("computer_search", "Full-text search across sessions (by task) and action logs (by reasoning)", {
|
|
19526
|
-
query: exports_external.string().describe("Search query"),
|
|
19527
|
-
scope: exports_external.enum(["sessions", "actions", "both"]).default("both").describe("Where to search"),
|
|
19528
|
-
limit: exports_external.number().default(20).describe("Max results")
|
|
19529
|
-
}, async (params) => {
|
|
19530
|
-
const results = {};
|
|
19531
|
-
if (params.scope === "sessions" || params.scope === "both") {
|
|
19532
|
-
results.sessions = searchSessions(params.query, params.limit);
|
|
19533
|
-
}
|
|
19534
|
-
if (params.scope === "actions" || params.scope === "both") {
|
|
19535
|
-
results.action_logs = searchActionLogs(params.query, params.limit);
|
|
19536
|
-
}
|
|
19537
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
19538
|
-
});
|
|
19539
|
-
server.tool("computer_stats", "Get usage statistics for computer use", {}, async () => {
|
|
19540
|
-
const stats = getStats();
|
|
19541
|
-
return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
|
|
19542
|
-
});
|
|
19543
|
-
server.tool("computer_accessibility", "Query the macOS accessibility tree \u2014 get structured UI elements (buttons, fields, labels) with positions. Much more precise than pixel-guessing from screenshots.", {
|
|
19544
|
-
app: exports_external.string().optional().describe("App name to query (default: frontmost)"),
|
|
19545
|
-
focused_only: exports_external.boolean().default(false).describe("Only get focused element's subtree"),
|
|
19546
|
-
depth: exports_external.number().default(3).describe("Max tree traversal depth"),
|
|
19547
|
-
format: exports_external.enum(["json", "summary"]).default("summary").describe("Output format")
|
|
19548
|
-
}, async (params) => {
|
|
19549
|
-
try {
|
|
19550
|
-
const elements = await queryAccessibilityTree({
|
|
19551
|
-
app: params.app,
|
|
19552
|
-
focusedOnly: params.focused_only,
|
|
19553
|
-
depth: params.depth
|
|
19554
|
-
});
|
|
19555
|
-
const text = params.format === "json" ? JSON.stringify(elements, null, 2) : summarizeAccessibilityTree(elements);
|
|
19556
|
-
return { content: [{ type: "text", text }] };
|
|
19557
|
-
} catch (err) {
|
|
19558
|
-
return { content: [{ type: "text", text: `Accessibility query failed: ${err instanceof Error ? err.message : err}` }] };
|
|
19914
|
+
if (url.pathname === "/mcp") {
|
|
19915
|
+
return handleMcpRequest(req);
|
|
19559
19916
|
}
|
|
19560
|
-
|
|
19561
|
-
|
|
19562
|
-
|
|
19563
|
-
|
|
19564
|
-
|
|
19565
|
-
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
|
|
19569
|
-
|
|
19570
|
-
|
|
19571
|
-
}
|
|
19572
|
-
|
|
19573
|
-
return {
|
|
19574
|
-
}
|
|
19575
|
-
|
|
19576
|
-
|
|
19577
|
-
focus: exports_external.string().describe("Current focus description")
|
|
19578
|
-
}, async (params) => {
|
|
19579
|
-
const ok = setFocus(params.agent_id, params.focus);
|
|
19580
|
-
return { content: [{ type: "text", text: ok ? "Focus updated" : "Agent not found" }] };
|
|
19581
|
-
});
|
|
19582
|
-
server.tool("computer_list_agents", "List all registered agents", {}, async () => {
|
|
19583
|
-
const agents = listAgents();
|
|
19584
|
-
return { content: [{ type: "text", text: JSON.stringify(agents, null, 2) }] };
|
|
19585
|
-
});
|
|
19586
|
-
server.tool("cloud_status", "Show computer cloud sync configuration and local sync history", {}, async () => cloudResult({
|
|
19587
|
-
configured: Boolean(getCloudDatabaseUrl()),
|
|
19588
|
-
env: [
|
|
19589
|
-
"HASNA_COMPUTER_CLOUD_DATABASE_URL",
|
|
19590
|
-
"OPEN_COMPUTER_CLOUD_DATABASE_URL",
|
|
19591
|
-
"COMPUTER_CLOUD_DATABASE_URL"
|
|
19592
|
-
],
|
|
19593
|
-
service: "computer",
|
|
19594
|
-
tables: CLOUD_TABLES,
|
|
19595
|
-
sync: getSyncMetaAll()
|
|
19596
|
-
}));
|
|
19597
|
-
server.tool("cloud_push", "Push local computer data to cloud PostgreSQL", { tables: exports_external.array(exports_external.string()).optional() }, async ({ tables }) => cloudResult(await cloudPush(tables ? { tables } : undefined)));
|
|
19598
|
-
server.tool("cloud_pull", "Pull computer data from cloud PostgreSQL to local SQLite", { tables: exports_external.array(exports_external.string()).optional() }, async ({ tables }) => cloudResult(await cloudPull(tables ? { tables } : undefined)));
|
|
19599
|
-
server.tool("cloud_sync", "Bidirectional computer sync: pull then push", { tables: exports_external.array(exports_external.string()).optional() }, async ({ tables }) => cloudResult(await cloudSync(tables ? { tables } : undefined)));
|
|
19917
|
+
return null;
|
|
19918
|
+
}
|
|
19919
|
+
async function startMcpHttpServer(port) {
|
|
19920
|
+
const httpServer = Bun.serve({
|
|
19921
|
+
hostname: "127.0.0.1",
|
|
19922
|
+
port,
|
|
19923
|
+
async fetch(req) {
|
|
19924
|
+
const handled = await handleMcpHttpRequest(req);
|
|
19925
|
+
if (handled)
|
|
19926
|
+
return handled;
|
|
19927
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
19928
|
+
}
|
|
19929
|
+
});
|
|
19930
|
+
return { port: httpServer.port, stop: () => httpServer.stop() };
|
|
19931
|
+
}
|
|
19932
|
+
|
|
19933
|
+
// src/mcp/index.ts
|
|
19600
19934
|
async function main() {
|
|
19601
|
-
const
|
|
19602
|
-
|
|
19935
|
+
const argv = process.argv.slice(2);
|
|
19936
|
+
if (isStdioMode(argv)) {
|
|
19937
|
+
const server = buildServer();
|
|
19938
|
+
const transport = new StdioServerTransport;
|
|
19939
|
+
await server.connect(transport);
|
|
19940
|
+
return;
|
|
19941
|
+
}
|
|
19942
|
+
const port = resolveHttpPort(argv);
|
|
19943
|
+
const { port: boundPort } = await startMcpHttpServer(port);
|
|
19944
|
+
console.error(`computer-mcp HTTP listening on http://127.0.0.1:${boundPort}/mcp`);
|
|
19603
19945
|
}
|
|
19604
|
-
|
|
19605
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
19946
|
+
if (import.meta.main) {
|
|
19947
|
+
main().catch((err) => {
|
|
19948
|
+
console.error("MCP server error:", err);
|
|
19949
|
+
process.exit(1);
|
|
19950
|
+
});
|
|
19951
|
+
}
|
|
19952
|
+
export {
|
|
19953
|
+
buildServer
|
|
19954
|
+
};
|