@hasna/computer 0.1.8 → 0.1.9

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.
Files changed (44) hide show
  1. package/LICENSE +4 -2
  2. package/README.md +73 -7
  3. package/dist/apps/ghostty/applescript.d.ts +36 -0
  4. package/dist/apps/ghostty/applescript.d.ts.map +1 -0
  5. package/dist/apps/ghostty/applescript.test.d.ts +2 -0
  6. package/dist/apps/ghostty/applescript.test.d.ts.map +1 -0
  7. package/dist/apps/ghostty/driver.d.ts +10 -0
  8. package/dist/apps/ghostty/driver.d.ts.map +1 -0
  9. package/dist/apps/registry.d.ts +5 -0
  10. package/dist/apps/registry.d.ts.map +1 -0
  11. package/dist/apps/registry.test.d.ts +2 -0
  12. package/dist/apps/registry.test.d.ts.map +1 -0
  13. package/dist/apps/types.d.ts +47 -0
  14. package/dist/apps/types.d.ts.map +1 -0
  15. package/dist/cli/index.js +1055 -91
  16. package/dist/cli/storage.d.ts +3 -0
  17. package/dist/cli/storage.d.ts.map +1 -0
  18. package/dist/cli/storage.test.d.ts +2 -0
  19. package/dist/cli/storage.test.d.ts.map +1 -0
  20. package/dist/db/storage-sync.d.ts +57 -0
  21. package/dist/db/storage-sync.d.ts.map +1 -0
  22. package/dist/db/storage-sync.test.d.ts +2 -0
  23. package/dist/db/storage-sync.test.d.ts.map +1 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +84 -26
  27. package/dist/mcp/http.d.ts +16 -0
  28. package/dist/mcp/http.d.ts.map +1 -0
  29. package/dist/mcp/http.test.d.ts +2 -0
  30. package/dist/mcp/http.test.d.ts.map +1 -0
  31. package/dist/mcp/index.d.ts +1 -1
  32. package/dist/mcp/index.d.ts.map +1 -1
  33. package/dist/mcp/index.js +609 -262
  34. package/dist/mcp/server.d.ts +4 -0
  35. package/dist/mcp/server.d.ts.map +1 -0
  36. package/dist/server/index.js +34565 -8747
  37. package/dist/storage.d.ts +5 -0
  38. package/dist/storage.d.ts.map +1 -0
  39. package/dist/storage.js +5519 -0
  40. package/package.json +7 -2
  41. package/dist/cli/cloud.d.ts +0 -3
  42. package/dist/cli/cloud.d.ts.map +0 -1
  43. package/dist/db/cloud-sync.d.ts +0 -33
  44. 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/cloud-sync.ts
19168
- var CLOUD_TABLES = ["sessions", "action_logs", "feedback"];
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 getCloudDatabaseUrl() {
19175
- return process.env["HASNA_COMPUTER_CLOUD_DATABASE_URL"] ?? process.env["OPEN_COMPUTER_CLOUD_DATABASE_URL"] ?? process.env["COMPUTER_CLOUD_DATABASE_URL"] ?? null;
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
- async function getCloudPg() {
19178
- const url = getCloudDatabaseUrl();
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 HASNA_COMPUTER_CLOUD_DATABASE_URL, OPEN_COMPUTER_CLOUD_DATABASE_URL, or COMPUTER_CLOUD_DATABASE_URL");
19397
+ throw new Error("Missing HASNA_COMPUTER_DATABASE_URL");
19181
19398
  }
19182
19399
  return new PgAdapterAsync(url);
19183
19400
  }
19184
- async function runCloudMigrations(remote) {
19401
+ async function runStorageMigrations(remote) {
19185
19402
  for (const sql of PG_MIGRATIONS)
19186
19403
  await remote.run(sql);
19187
19404
  }
19188
- async function cloudPush(options) {
19189
- const remote = await getCloudPg();
19405
+ async function storagePush(options) {
19406
+ const remote = await getStoragePg();
19190
19407
  const db2 = getDb();
19191
19408
  try {
19192
- await runCloudMigrations(remote);
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 cloudPull(options) {
19203
- const remote = await getCloudPg();
19419
+ async function storagePull(options) {
19420
+ const remote = await getStoragePg();
19204
19421
  const db2 = getDb();
19205
19422
  try {
19206
- await runCloudMigrations(remote);
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 cloudSync(options) {
19217
- const pull = await cloudPull(options);
19218
- const push2 = await cloudPush(options);
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 [...CLOUD_TABLES];
19229
- const allowed = new Set(CLOUD_TABLES);
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/index.ts
19356
- var server = new McpServer({
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
- server.tool("computer_run_task", "Run a computer use task \u2014 the AI sees your screen and controls mouse/keyboard to complete it", {
19364
- task: exports_external.string().describe("Natural language description of what to do"),
19365
- provider: exports_external.enum(["anthropic", "openai"]).default("anthropic").describe("AI provider"),
19366
- model: exports_external.string().optional().describe("Specific model to use"),
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.0"
19378
19592
  });
19379
- return {
19380
- content: [
19381
- {
19382
- type: "text",
19383
- text: JSON.stringify(session, null, 2)
19384
- }
19385
- ]
19386
- };
19387
- });
19388
- server.tool("computer_screenshot", "Capture a screenshot of the current screen", {
19389
- save_to: exports_external.string().optional().describe("Optional file path to save the screenshot")
19390
- }, async (params) => {
19391
- const ss = await captureScreenshot();
19392
- if (params.save_to) {
19393
- const dir = params.save_to.substring(0, params.save_to.lastIndexOf("/"));
19394
- const file = params.save_to.substring(params.save_to.lastIndexOf("/") + 1);
19395
- await saveScreenshotToFile(ss, dir, file);
19396
- }
19397
- return {
19398
- content: [
19399
- {
19400
- type: "image",
19401
- data: ss.base64,
19402
- mimeType: "image/png"
19403
- },
19404
- {
19405
- type: "text",
19406
- text: `Screen: ${ss.size.width}x${ss.size.height}`
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
- server.tool("computer_click", "Click at a specific screen coordinate", {
19412
- x: exports_external.number().describe("X coordinate"),
19413
- y: exports_external.number().describe("Y coordinate"),
19414
- button: exports_external.enum(["left", "right", "middle"]).default("left").describe("Mouse button"),
19415
- count: exports_external.number().default(1).describe("Click count (1=single, 2=double, 3=triple)")
19416
- }, async (params) => {
19417
- const result = await executeAction({
19418
- type: "click",
19419
- point: { x: params.x, y: params.y },
19420
- button: params.button,
19421
- count: params.count
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
- const content = [{ type: "text", text: result.success ? "Click executed" : `Click failed: ${result.error}` }];
19424
- if (result.screenshot) {
19425
- content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
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
- return { content };
19428
- });
19429
- server.tool("computer_type", "Type text using the keyboard", {
19430
- text: exports_external.string().describe("Text to type")
19431
- }, async (params) => {
19432
- const result = await executeAction({ type: "type", text: params.text });
19433
- const content = [{ type: "text", text: result.success ? "Text typed" : `Type failed: ${result.error}` }];
19434
- if (result.screenshot) {
19435
- content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
19436
- }
19437
- return { content };
19438
- });
19439
- server.tool("computer_key", "Press a key or key combination (e.g. 'enter', 'cmd+c', 'ctrl+shift+a')", {
19440
- keys: exports_external.string().describe("Key or combination to press")
19441
- }, async (params) => {
19442
- const result = await executeAction({ type: "key", keys: params.keys });
19443
- const content = [{ type: "text", text: result.success ? "Key pressed" : `Key failed: ${result.error}` }];
19444
- if (result.screenshot) {
19445
- content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
19446
- }
19447
- return { content };
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
- const content = [{ type: "text", text: result.success ? "Scrolled" : `Scroll failed: ${result.error}` }];
19463
- if (result.screenshot) {
19464
- content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/png" });
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
- return { content };
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
- server.tool("computer_register_agent", "Register an agent for multi-agent coordination", {
19562
- name: exports_external.string().describe("Agent name"),
19563
- description: exports_external.string().optional().describe("Agent description"),
19564
- capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities")
19565
- }, async (params) => {
19566
- const agent = registerAgent(params);
19567
- return { content: [{ type: "text", text: JSON.stringify(agent, null, 2) }] };
19568
- });
19569
- server.tool("computer_heartbeat", "Send a heartbeat to mark an agent as active", {
19570
- agent_id: exports_external.string().describe("Agent ID")
19571
- }, async (params) => {
19572
- const ok = heartbeat(params.agent_id);
19573
- return { content: [{ type: "text", text: ok ? "Heartbeat received" : "Agent not found" }] };
19574
- });
19575
- server.tool("computer_set_focus", "Set what an agent is currently focused on", {
19576
- agent_id: exports_external.string().describe("Agent ID"),
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 transport = new StdioServerTransport;
19602
- await server.connect(transport);
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
- main().catch((err) => {
19605
- console.error("MCP server error:", err);
19606
- process.exit(1);
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
+ };