@vortex-os/computer-use 0.1.0 → 0.2.1
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/package.json +9 -1
- package/scripts/mcp-stdio.mjs +88 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vortex-os/computer-use",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Add-on — read-only screen perception (structured UIA tree + pixel fallback + change watch) exposed as an MCP server, layered on @vortex-os/base. Windows-first. Control (mouse/keyboard) is intentionally out of scope.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vortex-os-project",
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"vortex-mcp-computer-use": "scripts/mcp-stdio.mjs"
|
|
26
26
|
},
|
|
27
|
+
"vortex": {
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"vortex-computer-use": {
|
|
30
|
+
"command": "node",
|
|
31
|
+
"args": ["node_modules/@vortex-os/computer-use/scripts/mcp-stdio.mjs"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
27
35
|
"scripts": {
|
|
28
36
|
"verify": "node scripts/verify.mjs"
|
|
29
37
|
},
|
package/scripts/mcp-stdio.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// computer-use — MCP stdio server (
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
// @vortex-os/computer-use — read-only screen-perception MCP stdio server (Windows-first).
|
|
3
|
+
// Tools: probe · read_ui · capture_screen · watch_capture · poll_change · beep. Control is out of scope.
|
|
4
|
+
// Two modes (bin `vortex-mcp-computer-use`):
|
|
5
|
+
// - default: run the stdio server (what an MCP host launches).
|
|
6
|
+
// - `install`: self-register into the project `.mcp.json` under the non-reserved key
|
|
7
|
+
// `vortex-computer-use` (merge-safe). e.g. `npx vortex-mcp-computer-use install`.
|
|
8
|
+
// Optional dep: @modelcontextprotocol/sdk — loaded DYNAMICALLY only on the serve path (see the
|
|
9
|
+
// bottom dispatch), so `install` registers and exits without needing the SDK present.
|
|
9
10
|
import { spawnSync, spawn } from 'node:child_process';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import { dirname, join } from 'node:path';
|
|
12
|
-
import { readFileSync, unlinkSync, statSync, mkdtempSync, rmSync, existsSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
13
|
+
import { readFileSync, unlinkSync, statSync, mkdtempSync, rmSync, existsSync, mkdirSync, writeFileSync, renameSync, appendFileSync } from 'node:fs';
|
|
13
14
|
import { tmpdir, homedir } from 'node:os';
|
|
14
15
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
15
16
|
|
|
@@ -408,11 +409,8 @@ const TOOLS = [
|
|
|
408
409
|
},
|
|
409
410
|
];
|
|
410
411
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
414
|
-
|
|
415
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
412
|
+
// The CallTool handler — standalone (no SDK types), wired to the server only on the serve path.
|
|
413
|
+
async function handleCallTool(req) {
|
|
416
414
|
const { name, arguments: a = {} } = req.params;
|
|
417
415
|
const useWorker = plat === 'win32'; // the resident worker is Windows PowerShell backend only
|
|
418
416
|
let result;
|
|
@@ -534,13 +532,86 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
534
532
|
} finally {
|
|
535
533
|
if (reqDir) { try { rmSync(reqDir, { recursive: true, force: true }); } catch {} }
|
|
536
534
|
}
|
|
537
|
-
}
|
|
535
|
+
}
|
|
538
536
|
|
|
539
537
|
// Clean up the worker on server shutdown (it would also auto-terminate via stdin EOF when the parent dies, but do it explicitly).
|
|
540
538
|
process.on('exit', () => workerMgr.dispose());
|
|
541
539
|
process.on('SIGINT', () => process.exit(0));
|
|
542
540
|
process.on('SIGTERM', () => process.exit(0));
|
|
543
541
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
542
|
+
// ── install mode: self-register into the project .mcp.json (stage 1) ─────────
|
|
543
|
+
// `vortex-mcp-computer-use install [--path <file>]` — merge-safe registration. ALWAYS uses the
|
|
544
|
+
// non-reserved key "vortex-computer-use" (the host reserves "computer-use" and silently won't load
|
|
545
|
+
// it), preserves every other server + top-level field, and refuses to overwrite a malformed file.
|
|
546
|
+
const SERVER_KEY = 'vortex-computer-use';
|
|
547
|
+
const SERVER_ENTRY = { command: 'node', args: ['node_modules/@vortex-os/computer-use/scripts/mcp-stdio.mjs'] };
|
|
548
|
+
|
|
549
|
+
const isObjLit = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
550
|
+
const refuse = (msg) => { process.stderr.write(`[install] ${msg}\n`); process.exit(1); };
|
|
551
|
+
|
|
552
|
+
function runInstall(argv) {
|
|
553
|
+
const i = argv.indexOf('--path');
|
|
554
|
+
let target;
|
|
555
|
+
if (i >= 0) {
|
|
556
|
+
const v = argv[i + 1];
|
|
557
|
+
if (!v || v.startsWith('--')) refuse('--path requires a file path argument.');
|
|
558
|
+
target = v;
|
|
559
|
+
} else {
|
|
560
|
+
target = join(process.cwd(), '.mcp.json');
|
|
561
|
+
}
|
|
562
|
+
let existing = {};
|
|
563
|
+
if (existsSync(target)) {
|
|
564
|
+
let txt;
|
|
565
|
+
try { txt = readFileSync(target, 'utf8').trim(); }
|
|
566
|
+
catch (e) { refuse(`${target} could not be read — left untouched: ${e.message}`); }
|
|
567
|
+
if (txt) {
|
|
568
|
+
let parsed;
|
|
569
|
+
try { parsed = JSON.parse(txt); }
|
|
570
|
+
catch (e) { refuse(`${target} is not valid JSON — refusing to overwrite (fix it first): ${e.message}`); }
|
|
571
|
+
if (!isObjLit(parsed)) refuse(`${target} is not a JSON object — refusing to overwrite.`);
|
|
572
|
+
if (parsed.mcpServers !== undefined && !isObjLit(parsed.mcpServers)) refuse(`${target} has a non-object "mcpServers" — refusing to overwrite.`);
|
|
573
|
+
existing = parsed;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const servers = isObjLit(existing.mcpServers) ? existing.mcpServers : {};
|
|
577
|
+
// ADD-ONLY: never clobber an existing "vortex-computer-use" entry (the user may have customized it).
|
|
578
|
+
if (Object.prototype.hasOwnProperty.call(servers, SERVER_KEY)) {
|
|
579
|
+
process.stdout.write(JSON.stringify({ ok: true, action: 'already-present', path: target, serverKey: SERVER_KEY }, null, 2) + '\n');
|
|
580
|
+
process.stderr.write(`[install] "${SERVER_KEY}" already registered in ${target} — left unchanged.\n`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const preserved = Object.keys(servers);
|
|
584
|
+
const merged = { ...existing, mcpServers: { ...servers, [SERVER_KEY]: SERVER_ENTRY } };
|
|
585
|
+
// Atomic write via a PRIVATE temp dir (random name, not a guessable sibling): write inside with
|
|
586
|
+
// exclusive-create, rename onto the target, then remove the dir. Avoids following a pre-placed
|
|
587
|
+
// temp symlink and leaves no stray temp on failure (codex r2 MEDIUM).
|
|
588
|
+
try { mkdirSync(dirname(target), { recursive: true }); } catch {}
|
|
589
|
+
const tmpDir = mkdtempSync(join(dirname(target), '.mcp-cu-'));
|
|
590
|
+
const tmp = join(tmpDir, 'mcp.json');
|
|
591
|
+
try {
|
|
592
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
|
|
593
|
+
renameSync(tmp, target);
|
|
594
|
+
} finally {
|
|
595
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
596
|
+
}
|
|
597
|
+
process.stdout.write(JSON.stringify({ ok: true, action: 'added', path: target, serverKey: SERVER_KEY, preservedServers: preserved }, null, 2) + '\n');
|
|
598
|
+
process.stderr.write(
|
|
599
|
+
`[install] added "${SERVER_KEY}" in ${target}` +
|
|
600
|
+
(preserved.length ? ` (kept: ${preserved.join(', ')})` : '') + '\n' +
|
|
601
|
+
`[install] Restart the agent — or approve the new MCP server when prompted — to load the computer-use tools.\n`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (process.argv.slice(2).includes('install')) {
|
|
606
|
+
runInstall(process.argv.slice(2));
|
|
607
|
+
} else {
|
|
608
|
+
// Serve path: load the MCP SDK dynamically (so `install` never requires it), wire the handlers, connect.
|
|
609
|
+
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
610
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
611
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = await import('@modelcontextprotocol/sdk/types.js');
|
|
612
|
+
const server = new Server({ name: 'computer-use', version: '0.2.1' }, { capabilities: { tools: {} } });
|
|
613
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
614
|
+
server.setRequestHandler(CallToolRequestSchema, handleCallTool);
|
|
615
|
+
await server.connect(new StdioServerTransport());
|
|
616
|
+
process.stderr.write(`[computer-use MCP] ready on stdio (worker=${plat === 'win32' ? 'on' : 'off'}; tools: probe, read_ui, capture_screen, watch_capture, poll_change, beep)\n`);
|
|
617
|
+
}
|